Golang的aws AWS服务电话
#aws #go #mock #stubbing

我最近改用Golang选择了我选择的语言。 (在我以前的blog中,您可以阅读原因。)但是我也是测试驱动开发的忠实拥护者。使用Python,您有一个stubber,可以帮助您嘲笑AWS API。那么,您如何在Golang中这样做?我找到了两种方法。一种是通过依赖注射,一种是通过僵硬的。在此博客中,我将分享到目前为止的经验。

使用依赖注入

我的第一个实验是依赖注射。我使用以下代码来做到这一点:

package main

import (
    "context"
    "github.com/aws/aws-sdk-go-v2/config"
    "github.com/aws/aws-sdk-go-v2/service/s3"
    "time"
    "log"
    "os"
)

type Request struct {}
type Response struct {}

type Lambda struct {
    s3Client *s3.Client
}

func New() (*Lambda, error) {
    cfg, err := config.LoadDefaultConfig(context.TODO())

    m := new(Lambda)
    m.SetS3Client(s3.NewFromConfig(cfg))
    return m, err
}

func (x *Lambda) SetS3Client(client *s3.Client) {
    x.s3Client = client
}

func (x *Lambda) Handler(ctx context.Context, request Request) (Response, error) {
    data := []byte("Hello World")

    _, err := x.s3Client.PutObject(x.ctx, &s3.PutObjectInput{
        Bucket: aws.String("my-bucket"),
        Key:    aws.String("my-object-key"),
        Body:   bytes.NewReader(data),
    })

    return Response{}, err
}

您可以看到,我将SetS3Client方法用作二元组。它使我可以从外部设置客户。进行单元测试时,这很有用。您可以在测试中使用它:

package main

import (
    "context"
    "github.com/aws/aws-sdk-go-v2/service/s3"
    "testing"
    "time"
    "log"
    "os"
)

type mockS3Client struct {
    s3.Client
    Error error
}

func (m *mockS3Client) PutObject(input *s3.PutObjectInput) (*s3.PutObjectOutput, error) {
    return &s3.PutObjectOutput{}, nil
}

func TestHandler(t *testing.T) {
    lambda := New()
    lambda.SetS3Client(&mockS3Client{})
    var ctx = context.Background()
    var event Request

    t.Run("Invoke Handler", func(t *testing.T) {
        response, err := lambda.Handler(ctx, event)

        // Perform Assertions
    })
}

我们注入一个模拟的对象,该对象充当用于执行API调用的客户端。通过这种方法,我现在可以编写一些测试。但是我意识到这种方法会造成另一个问题。例如,如果您有2个执行PutObject调用的API调用该怎么办?在此示例中,我返回一个空的PutObjectOutput。但是我想测试多个方案,那么您如何在模拟的对象中控制此行为?

使用顽固的

所以我做了更多的研究,发现了awsdocs/aws-doc-sdk-examples回购。该存储库使用了testtools模块。因此,我开始了一个实验,以了解如何使用此模块。我按照以下内容进行了重构:

package main

import (
    "context"
    "github.com/aws/aws-sdk-go-v2/aws"
    "github.com/aws/aws-sdk-go-v2/service/s3"
)

type Request struct {}
type Response struct {}

type Lambda struct {
    ctx      context.Context
    s3Client *s3.Client
}

func New(cfg aws.Config) *Lambda {
    m := new(Lambda)
    m.s3Client = s3.NewFromConfig(cfg)
    return m
}

func (x *Lambda) Handler(ctx context.Context, request Request) (Response, error) {
    data := []byte("Hello World")

    _, err := x.s3Client.PutObject(x.ctx, &s3.PutObjectInput{
        Bucket: aws.String("my-bucket"),
        Key:    aws.String("my-object-key"),
        Body:   bytes.NewReader(data),
    })

    return Response{}, err
}

我在New方法中添加了一个cfg参数,所以我还需要以我的主要方法传递。

package main

import (
    "context"
    "github.com/aws/aws-lambda-go/lambda"
    "github.com/aws/aws-sdk-go-v2/config"
    "log"
)

func main() {
    cfg, err := config.LoadDefaultConfig(context.TODO())
    if err != nil {
        log.Printf("error: %v", err)
        return
    }
    lambda.Start(New(cfg).Handler)
}

测试本身看起来像这样:

package main

import (
    "bytes"
    "context"
    "encoding/json"
    "errors"
    "github.com/aws/aws-sdk-go-v2/aws"
    "github.com/aws/aws-sdk-go-v2/service/s3"
    "github.com/awsdocs/aws-doc-sdk-examples/gov2/testtools"
    "io"
    "os"
    "strings"
    "testing"
)


func TestHandler(t *testing.T) {
    var ctx = context.Background()
    var event Request

    t.Run("Upload a file to S3", func(t *testing.T) {
        stubber := testtools.NewStubber()
        lambda := New(*stubber.SdkConfig)

        stubber.Add(testtools.Stub{
            OperationName: "PutObject",
            Input: &s3.PutObjectInput{
                Bucket: aws.String("my-sample-bucket"),
                Key:    aws.String("my/object.json"),
                Body:   bytes.NewReader([]byte{}),
            },
            Output: &s3.PutObjectOutput{},
        })

        response, err := lambda.Handler(ctx, event)
        testtools.ExitTest(stubber, t)

        // Perform Assertions
    })
}

您可以看到,我们现在在测试本身中移动了模拟。这使您可以根据测试使AWS API反应。最大的优点是它封装在测试本身中。例如,如果要添加一个场景,则PutObject调用失败,则添加以下内容:

t.Run("Fail on upload", func(t *testing.T) {
    stubber := testtools.NewStubber()
    lambda := New(*stubber.SdkConfig)
    raiseErr := &testtools.StubError{Err: errors.New("ClientError")}

    stubber.Add(testtools.Stub{
        OperationName: "PutObject",
        Input: &s3.PutObjectInput{
            Bucket: aws.String("my-sample-bucket"),
            Key:    aws.String("my/object.json"),
            Body:   bytes.NewReader([]byte{}),
        },
        Error: raiseErr,
    })

    _, err := lambda.Handler(ctx, event)
    testtools.VerifyError(err, raiseErr, t)
    testtools.ExitTest(stubber, t)
})

Stubber的主要优点是您可以从开箱即用验证Input。但是在某些情况下,您想忽略某些领域。例如,如果您在Key中使用时间戳。或您要上传的对象的实际Body

您可以通过设置IgnoreFields忽略这些字段。 (示例:IgnoreFields: []string{"Key", "Body"}

我喜欢使用Stubber的第二件事是VerifyError方法。这将验证您是否在测试方案中提出的错误是否返回。

最后一个是ExitTest方法。这将确保实际调用所有定义的存根。换句话说,如果您仍然有存根。您的测试会失败,因为仍然有一个未符合的存根。

结论

testtool是我在python中使用的固执的一个很好的替代。它使您可以将场景数据封装在测试中。避免难以维护模拟对象。 testtool从配置中起作用,因此您无需固执。导致更少的代码来测试您的实施。

Klaus Nielsen