AWS SDK for Go v2 のユニッテスト (モックテスト)

May 29, 2020 - 読了時間 2 分
aws testing golang

aws-sdk-go-v2 を使ったユニットテストについて紹介します。

TL/DR

ここで紹介するユニットテストのサンプルコードは次のリポジトリにあります。

実際のコードをみた方が手っ取り早いという方はリポジトリをクローンしてテストを実行してみてください。

$ cd aws-sdk-go-v2-sample/
$ make test

s3mock というディレクトリ配下に S3 のモックコードが生成されます。 そのモックを使う s3fs_test.go がテストコードになります。

$ tree client/
client/
├── s3fs
│   ├── s3client.go
│   ├── s3fs.go
│   ├── s3fs_test.go
│   └── s3util.go
├── s3mock
│   ├── s3.go
│   └── s3manager.go
└── s3mock_test.go

インターフェースからモックを生成

AWS Developer Blog に aws-sdk-go でユニットテストをするための方法が紹介されています。 少し古い記事なので v2 ではなく v1 を使っていますが、基本的には同じやり方でできます。

ここでは gomock というツールでモックを生成しています。 gomock が提供している mockgen CLI を使ってモックのコード生成を行うこともできますが、 上述したサンプルコードでは go generate コマンドでモックのコードを生成しています。

例えば s3mock_test.gogo generate 向けのコメントがあります。

$ cat client/s3mock_test.go
package client_test

//go:generate mockgen -package s3mock -destination s3mock/s3.go github.com/aws/aws-sdk-go-v2/service/s3/s3iface ClientAPI
//go:generate mockgen -package s3mock -destination s3mock/s3manager.go github.com/aws/aws-sdk-go-v2/service/s3/s3manager/s3manageriface UploaderAPI

実際に go generate を実行することでモックのコードを生成します。

$ go generate -v -x client/s3mock_test.go
client/s3mock_test.go
mockgen -package s3mock -destination s3mock/s3.go github.com/aws/aws-sdk-go-v2/service/s3/s3iface ClientAPI
mockgen -package s3mock -destination s3mock/s3manager.go github.com/aws/aws-sdk-go-v2/service/s3/s3manager/s3manageriface UploaderAPI

次のようなモックが生成されます。

$ cat client/s3mock/s3.go 
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/aws/aws-sdk-go-v2/service/s3/s3iface (interfaces: ClientAPI)

// Package s3mock is a generated GoMock package.
package s3mock

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

// MockClientAPI is a mock of ClientAPI interface
type MockClientAPI struct {
    ctrl     *gomock.Controller
    recorder *MockClientAPIMockRecorder
}
...

モックを使ったユニットテスト

例えば、次のような HeadObject リクエストを行うコードをテストしたいと仮定します。

func (c *S3Client) HeadObject(bucket, key string) error {
    input := &s3.HeadObjectInput{
        Bucket: aws.String(bucket),
        Key:    aws.String(key),
    }
    req := c.svc.HeadObjectRequest(input)
    _, err := req.Send(context.Background())
    return err
}

v1 とは異なり、v2 では s3.HeadObjectRequest を作成して、 その Send() メソッドを呼び出すように変わりました。 この変更により、モックを使ったユニットテストで一手間かかるようになります。

現時点では、この課題はまだ解決されておらず、 次の issue でワークアラウンドの方法が紹介されています。

実際にモックを使ったテストコードを紹介します。 次のコードは s3 の HeadObject の API に対するレスポンスをモックに登録しています。

func newMockS3(t *testing.T) (*s3mock.MockClientAPI) {
    ctrl := gomock.NewController(t)
    svc := s3mock.NewMockClientAPI(ctrl)
    output := &s3.HeadObjectOutput{}
    svc.EXPECT().
        HeadObjectRequest(gomock.Any()).
        Return(mockHeadObjectRequest(output, nil))

一手間というのは、mockHeadObjectRequest() のところです。 この関数は任意の s3.HeadObjectOutput を返す s3.HeadObjectRequest を作成しています。 内部的に aws.Request も作成する必要があります。

func mockHeadObjectRequest(
    output *s3.HeadObjectOutput, err error,
) s3.HeadObjectRequest {
    req := &aws.Request{
        HTTPRequest: &http.Request{},
        Retryer:     aws.NoOpRetryer{},
        Data:        output,
        Error:       err,
    }
    return s3.HeadObjectRequest{
        Request: req,
    }
}

モックを使うコードそのものが煩雑になりがちなのでそんなに手間ではないものの、 内部仕様を知っていないと書けないようなモックはあまり望ましくありません。 上記の issue が解決すれば、 いずれ mockHeadObjectRequest() のようなコードが不要になることでしょう。

BizPy コミュニティの紹介 Presto のビルド環境を構築する

関連記事

Hugo で SNS 向けのシェアボタンを追加する

August 22, 2020 - 読了時間 1 分
hugo golang template

Hugo で前後の記事へのリンクを追加する

August 21, 2020 - 読了時間 1 分
hugo golang template

Hugo の Shortcodes テンプレートを使う

August 1, 2020 - 読了時間 1 分
hugo golang template