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.go
に go 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()
のようなコードが不要になることでしょう。