为gcp compute sdk创建一个mock



我使用以下函数,我需要引发它的覆盖率(如果可能到100%),问题是我通常使用interface来处理Go中的这种情况,对于这个特定的情况不确定如何做到这一点,因为这是一个更棘手的你知道吗?

The packagehttps://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
}

每当我发现自己处于这种情况时,我发现最简单的解决方案是自己创建缺少接口的。我将这些接口限制为我正在使用的类型和函数,而不是为整个库编写接口。然后,在我的代码中,我不接受第三方具体类型,而是接受这些类型的接口。然后,我像往常一样使用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.AddressesClient(Close除外)的方法是并发安全的,

您可以创建一个客户端,并为每次调用或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包没有为其提供工厂函数,所以您不能容易地控制从List返回的迭代器在测试中的行为。一种解决方法是声明一个额外的接口

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
}
}

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

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 {
// ...
}
}

最新更新