去为 gcp compute sdk 创建一个 mock

Go create a mock for gcp compute sdk

我使用下面的函数,我需要提高它的覆盖率(如果可能到100%),问题是我通常使用interface在 Go 中处理此类情况,对于这种特定情况,不确定如何处理,因为这有点 tricky,知道吗?

The package https://pkg.go.dev/google.golang.org/genproto/googleapis/cloud/compute/v1

我使用的哪个没有 interface 所以不确定如何模拟它?

import (
    "context"
    "errors"
    "fmt"
    "os"

    compute "cloud.google.com/go/compute/apiv1"
    "google.golang.org/api/iterator"
    "google.golang.org/api/option"
    computev1 "google.golang.org/genproto/googleapis/cloud/compute/v1"
)

func Res(ctx context.Context, project string, region string,vpc string,secret string) error {

    c, err := compute.NewAddressesRESTClient(ctx, option.WithCredentialsFile(secret))

    if err != nil {
        return err
    }

    defer c.Close()
    addrReq := &computev1.ListAddressesRequest{
        Project: project,
        Region:  region,
    }
    it := c.List(ctx, addrReq)

    for {
        resp, err := it.Next()
        if err == iterator.Done {
            break
        }
        if err != nil {
            return err
        }
        if *(resp.Status) != "IN_USE" {
            return ipConverter(*resp.Name, vpc)
        }
    }
    return nil
}

每当我遇到这种情况时,我发现最简单的解决方案是自己创建 missing 接口。我将这些接口限制为我正在使用的类型和函数,而不是为整个库编写接口。然后,在我的代码中,我不接受 third-party 具体类型,而是接受这些类型的接口。然后我像往常一样使用 gomock 为这些接口生成模拟。

以下是受您的代码启发的描述性示例。

type RestClient interface {
    List(context.Context, *computev1.ListAddressesRequest) (ListResult, error) // assuming List returns ListResult type.
    Close() error
}

func newRestClient(ctx context.Context, secret string) (RestClient, error) {
    return compute.NewAddressesRESTClient(ctx, option.WithCredentialsFile(secret))
}

func Res(ctx context.Context, project string, region string, vpc string, secret string) error {
    c, err := newRestClient(ctx, secret)
    if err != nil {
        return err
    }

    defer c.Close()
    return res(ctx, project, region, vpc, c)
}

func res(ctx context.Context, project string, region string, vpc string, c RestClient) error {
    addrReq := &computev1.ListAddressesRequest{
        Project: project,
        Region:  region,
    }

    it, err := c.List(ctx, addrReq)
    if err != nil {
        return err
    }

    for {
        resp, err := it.Next()
        if err == iterator.Done {
            break
        }

        if err != nil {
            return err
        }

        if *(resp.Status) != "IN_USE" {
            return ipConverter(*resp.Name, vpc)
        }
    }

    return nil
}

现在您可以通过向内部 res 函数注入模拟 RestClient 来测试 Res 函数的重要部分。

此处可测试性的一个障碍是您在 Res 函数中实例化客户端而不是注入它。因为

  • 秘密在程序的生命周期内不会改变,
  • *compute.AddressesClientClose除外)的方法是concurrency-safe、

您可以创建一个客户端并在每次调用时重复使用它或 Res。要将其注入 Res,您可以声明一些 Compute 类型并将 Res 转换为该类型的方法:

type Compute struct {
  Lister Lister // some appropriate interface type
}

func (cp *Compute) Res(ctx context.Context, project, region, vpc string) error {
addrReq := &computev1.ListAddressesRequest{
        Project: project,
        Region:  region,
    }
    it := cp.Lister.List(ctx, addrReq)
    for {
        resp, err := it.Next()
        if err == iterator.Done {
            break
        }
        if err != nil {
            return err
        }
        if *(resp.Status) != "IN_USE" {
            return ipConverter(*resp.Name, vpc)
        }
    }
    return nil
}

还有一个问题:您应该如何申报 Lister?一种可能是

type Lister interface {
    List(ctx context.Context, req *computev1.ListAddressesRequest, opts ...gax.CallOption) *compute.AddressIterator
}

但是,因为 compute.AddressIterator 是一个结构类型,有一些未导出的字段,包 compute 没有提供工厂函数,所以你不能轻易控制迭代器如何从 [=28= 返回] 在你的测试中表现。一种出路是声明一个额外的接口,

type Iterator interface {
    Next() (*computev1.Address, error)
}

并将 List 的结果类型从 *compute.AddressIterator 更改为 Iterator:

type Lister interface {
    List(ctx context.Context, req *computev1.ListAddressesRequest, opts ...gax.CallOption) Iterator
}

然后你可以为真正的 Lister 声明另一个结构类型并在生产端使用它:

type RealLister struct {
    Client *compute.AddressesClient
}

func (rl *RealLister) List(ctx context.Context, req *computev1.ListAddressesRequest, opts ...gax.CallOption) Iterator {
    return rl.Client.List(ctx, req, opts...)
}

func main() {
    secret := "don't hardcode me"
    ctx, cancel := context.WithCancel(context.Background()) // for instance
    defer cancel()
    c, err := compute.NewAddressesRESTClient(ctx, option.WithCredentialsFile(secret))
    if err != nil {
        log.Fatal(err) // or deal with the error in some way
    }
    defer c.Close()
    cp := Compute{Lister: &RealLister{Client: c}}
    if err := cp.Res(ctx, "my-project", "us-east-1", "my-vpc"); err != nil {
        log.Fatal(err) // or deal with the error in some way
    }
}

对于您的测试,您可以声明另一种结构类型,它将充当可配置的 测试替身:

type FakeLister func(ctx context.Context, req *computev1.ListAddressesRequest, opts ...gax.CallOption) Iterator

func (fl FakeLister) List(ctx context.Context, req *computev1.ListAddressesRequest, opts ...gax.CallOption) Iterator {
    return fl(ctx, req, opts...)
}

要在测试中控制 Iterator 的行为,您可以声明另一个可配置的具体类型:

type FakeIterator struct{
    Err error
    Status string
}

func (fi *FakeIterator) Next() (*computev1.Address, error) {
    addr := computev1.Address{Status: &fi.Status}
    return &addr, fi.Err
}

测试函数可能如下所示:

func TestResStatusInUse(t *testing.T) {
    // Arrange
    l := func(_ context.Context, _ *computev1.ListAddressesRequest, _ ...gax.CallOption) Iterator {
        return &FakeIterator{
            Status: "IN_USE",
            Err:    nil,
        }
    }
    cp := Compute{Lister: FakeLister(l)}
    dummyCtx := context.Background()
    // Act
    err := cp.Res(dummyCtx, "my-project", "us-east-1", "my-vpc")
    // Assert
    if err != nil {
        // ...
    }
}