去为 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.AddressesClient
(Close
除外)的方法是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 {
// ...
}
}
我使用下面的函数,我需要提高它的覆盖率(如果可能到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.AddressesClient
(Close
除外)的方法是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 {
// ...
}
}