AWS SDK for Go V2でのユニットテスト
やること
公式のドキュメントを参考にしてAWS SDK for Go V2のユニットテストを理解する
準備
今回使用するGoのバージョン:
% go version
go version go1.22.0 darwin/arm64
プロジェクトの初期化:
<github account>
と<repository>
は環境に合わせて変更。
% go mod init github.com/<github account>/<repository>
go: creating new go.mod: module github.com/<github account>/<repository>
go.mod
ファイルが作成されている。
最終的なファイル構成
% tree
.
├── go.mod
├── go.sum
├── main.go
└── main_test.go
サンプルコードの実装
main.go
を作成し、公式のサンプルコードを参考にして下記のコードを追加する。
package main
import (
"context"
"io"
"github.com/aws/aws-sdk-go-v2/service/s3"
)
type S3GetObjectAPI interface {
GetObject(ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.Options)) (*s3.GetObjectOutput, error)
}
func GetObjectFromS3(ctx context.Context, api S3GetObjectAPI, bucket, key string) ([]byte, error) {
object, err := api.GetObject(ctx, &s3.GetObjectInput{
Bucket: &bucket,
Key: &key,
})
if err != nil {
return nil, err
}
defer object.Body.Close()
return io.ReadAll(object.Body)
}
サンプルとの違いは
-
package main
を追加 -
ioutil.ReadAll(object.Body)
はDEPRECATED
(非推奨)なので、io.ReadAll(object.Body)
に変更
その後、
go mod tidy
を実行し、"github.com/aws/aws-sdk-go-v2/service/s3"
をダウンロードする。
ポイント
ここでのポイントは、S3GetObjectAPI
interfaceを定義して、GetObjectFromS3
へのDependency Injection
(依存性の注入)ができるようなっていること。
後のテストコードで、S3GetObjectAPI
interfaceを満たす、mockを定義する。
aws-sdk-go-v2のドキュメントについて
ユニットテストに進む前に、S3GetObjectAPI
interfaceのGetObject
の関数のドキュメントを確認する。
aws-sdk-go-v2
のs3
のドキュメントはここにある:
func (*Client) GetObject
はこちら:
func (c *Client) GetObject(ctx context.Context, params *GetObjectInput, optFns ...func(*Options)) (*GetObjectOutput, error)
次の項目で、S3GetObjectAPI
interface定義を満たすように、mockGetObjectAPI
を
定義する。
テストの作成
公式サンプルを少し修正し、main_test.go
を作成。
-
package main
を追加 -
ioutil
はio
に変更。
package main
import (
"bytes"
"context"
"io"
"strconv"
"testing"
"github.com/aws/aws-sdk-go-v2/service/s3"
)
type mockGetObjectAPI func(ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.Options)) (*s3.GetObjectOutput, error)
func (m mockGetObjectAPI) GetObject(ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.Options)) (*s3.GetObjectOutput, error) {
return m(ctx, params, optFns...)
}
func TestGetObjectFromS3(t *testing.T) {
cases := []struct {
client func(t *testing.T) S3GetObjectAPI
bucket string
key string
expect []byte
}{
{
client: func(t *testing.T) S3GetObjectAPI {
return mockGetObjectAPI(func(ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.Options)) (*s3.GetObjectOutput, error) {
t.Helper()
if params.Bucket == nil {
t.Fatal("expect bucket to not be nil")
}
if e, a := "fooBucket", *params.Bucket; e != a {
t.Errorf("expect %v, got %v", e, a)
}
if params.Key == nil {
t.Fatal("expect key to not be nil")
}
if e, a := "barKey", *params.Key; e != a {
t.Errorf("expect %v, got %v", e, a)
}
return &s3.GetObjectOutput{
Body: io.NopCloser(bytes.NewReader([]byte("this is the body foo bar baz"))),
}, nil
})
},
bucket: "fooBucket",
key: "barKey",
expect: []byte("this is the body foo bar baz"),
},
}
for i, tt := range cases {
t.Run(strconv.Itoa(i), func(t *testing.T) {
ctx := context.TODO()
content, err := GetObjectFromS3(ctx, tt.client(t), tt.bucket, tt.key)
if err != nil {
t.Fatalf("expect no error, got %v", err)
}
if e, a := tt.expect, content; bytes.Compare(e, a) != 0 {
t.Errorf("expect %v, got %v", e, a)
}
})
}
}
テストの実行
テストを試してみる。
% go test ./...
ok github.com/<github account>/<repo name> 0.109s
テストはOK
何をしているのか
まず、mockGetObjectAPI
はGetObject
メソッドを持ち、S3GetObjectAPI
interfaceを満たしているので、Dependency Injection
が可能。
type mockGetObjectAPI func(ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.Options)) (*s3.GetObjectOutput, error)
func (m mockGetObjectAPI) GetObject(ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.Options)) (*s3.GetObjectOutput, error) {
return m(ctx, params, optFns...)
}
次に、GetObject
の中身を見てみると、m(ctx, params, optFns...)
をreturnしているだけである。
レシーバのmockGetObjectAPI
が関数型であるため、テストケースの中でmockGetObjectAPI
を実装し、振る舞いを制御できるようになる。
{
client: func(t *testing.T) S3GetObjectAPI {
return mockGetObjectAPI(func(ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.Options)) (*s3.GetObjectOutput, error) {
t.Helper()
if params.Bucket == nil {
t.Fatal("expect bucket to not be nil")
}
if e, a := "fooBucket", *params.Bucket; e != a {
t.Errorf("expect %v, got %v", e, a)
}
if params.Key == nil {
t.Fatal("expect key to not be nil")
}
if e, a := "barKey", *params.Key; e != a {
t.Errorf("expect %v, got %v", e, a)
}
return &s3.GetObjectOutput{
Body: io.NopCloser(bytes.NewReader([]byte("this is the body foo bar baz"))),
}, nil
})
},
bucket: "fooBucket",
key: "barKey",
expect: []byte("this is the body foo bar baz"),
},
このように、テストケースの中で、mockGetObjectAPI
の関数を実装している。
t.Helper()
if params.Bucket == nil {
t.Fatal("expect bucket to not be nil")
}
if e, a := "fooBucket", *params.Bucket; e != a {
t.Errorf("expect %v, got %v", e, a)
}
if params.Key == nil {
t.Fatal("expect key to not be nil")
}
if e, a := "barKey", *params.Key; e != a {
t.Errorf("expect %v, got %v", e, a)
}
return &s3.GetObjectOutput{
Body: io.NopCloser(bytes.NewReader([]byte("this is the body foo bar baz"))),
}, nil
バケット名とオブジェクトキーがs3.GetObjectInput
を通して正しく設定されているかをテストし、擬似的作ったオブジェクトが正しく読み込めるかをテストしている。
まとめ
公式サンプルコードの理解すべきポイントは2点:
-
type S3GetObjectAPI interface
を定義することで、Dependency Injection
できるようにしている -
type mockGetObjectAPI
が関数型で定義されており、これによりテストケースの中で振る舞いを変更できるようにしている
続編
Discussion