😀

AWS SDK for Go V2でのユニットテスト

2024/03/05に公開

やること

https://aws.github.io/aws-sdk-go-v2/docs/unit-testing/

公式のドキュメントを参考にして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のドキュメントについて

ユニットテストに進む前に、S3GetObjectAPIinterfaceのGetObjectの関数のドキュメントを確認する。

aws-sdk-go-v2s3のドキュメントはここにある:

https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/s3

func (*Client) GetObjectはこちら:

https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/s3#Client.GetObject

func (c *Client) GetObject(ctx context.Context, params *GetObjectInput, optFns ...func(*Options)) (*GetObjectOutput, error)

次の項目で、S3GetObjectAPI interface定義を満たすように、mockGetObjectAPI
定義する。

テストの作成

公式サンプルを少し修正し、main_test.goを作成。

  • package mainを追加
  • ioutilioに変更。
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

何をしているのか

まず、mockGetObjectAPIGetObjectメソッドを持ち、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が関数型で定義されており、これによりテストケースの中で振る舞いを変更できるようにしている

続編

https://zenn.dev/wac/articles/745a1711b0ce0d

Discussion