去为 GCP Compute SDK 创建一个模拟对象。

huangapple go评论76阅读模式
英文:

Go create a mock for gcp compute sdk

问题

我使用以下函数,并且需要提高其覆盖率(如果可能达到100%)。问题是,通常我在Go中使用interface来处理这种情况,但对于这个特定的情况,我不确定该如何做,因为这有点棘手。有什么想法吗?

我使用的包是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
}
英文:

I use the following function, and I need to raise the coverage of it (if possible to 100%), the problem is that typically I use interface to handle such cases in Go and for this specific case not sure how to do it, as this is a bit more tricky, any idea?

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

Which I use doesn't have interface so not sure how can I mock it?

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
}

答案1

得分: 3

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

以下是受你代码启发的一个描述性示例。

type RestClient interface {
	List(context.Context, *computev1.ListAddressesRequest) (ListResult, error) // 假设List返回ListResult类型。
	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函数的重要部分。

英文:

Whenever I find myself in this scenario, I found that the easiest solution is to create missing interfaces myself. I limit these interfaces to the types and functions that I am using, instead of writing interfaces for the entire library. Then, in my code, instead of accepting third-party concrete types, I accept my interfaces for those types. Then I use gomock to generate mocks for these interfaces as usual.

The following is a descriptive example inspired by your code.

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
}

Now you can test the important bits of the Res function by injecting a mock RestClient to the internal res function.

答案2

得分: 2

这里的一个测试障碍是你在Res函数内部实例化了一个客户端,而不是注入它。由于:

  • 密钥在程序的生命周期内不会改变,
  • *compute.AddressesClient的方法(除了Close)是并发安全的,

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

type Compute struct {
  Lister Lister // 一些适当的接口类型
}

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()) // 例如
  defer cancel()
  c, err := compute.NewAddressesRESTClient(ctx, option.WithCredentialsFile(secret))
  if err != nil {
    log.Fatal(err) // 或以某种方式处理错误
  }
  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) // 或以某种方式处理错误
  }
}

对于你的测试,你可以声明另一个结构类型,作为可配置的测试替身

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) {
  // 准备
  l := func(_ context.Context, _ *computev1.ListAddressesRequest, _ ...gax.CallOption) Iterator {
    return &FakeIterator{
      Status: "IN_USE",
      Err:    nil,
    }
  }
  cp := Compute{Lister: FakeLister(l)}
  dummyCtx := context.Background()
  // 执行
  err := cp.Res(dummyCtx, "my-project", "us-east-1", "my-vpc")
  // 断言
  if err != nil {
    // ...
  }
}
英文:

One obstacle to testability here is that you instantiate a client inside your Res function rather than injecting it. Because

  • the secret doesn't change during the lifetime of the programme,
  • the methods of *compute.AddressesClient (other than Close) are concurrency-safe,

you could create one client and reuse it for each invocation or Res. To inject it into Res, you can declare some Compute type and turn Res into a method on that type:

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
}

One question remains: how should you declare Lister? One possibility is

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

However, because compute.AddressIterator is a struct type with some unexported fields and for which package compute provides no factory function, you can't easily control how the iterator returned from List behaves in your tests. One way out is to declare an additional interface,

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

and change the result type of List from *compute.AddressIterator to Iterator:

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

Then you can declare another struct type for the real Lister and use that on the production side:

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

For your tests, you can declare another struct type that will act as a configurable 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...)
}

To control the behaviour of the Iterator in your test, you can declare another configurable concrete type:

type FakeIterator struct{
	Err error
	Status string
}

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

A test function may look like this:

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

huangapple
  • 本文由 发表于 2022年4月28日 23:55:25
  • 转载请务必保留本文链接:https://go.coder-hub.com/72046805.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定