テストでaws-sdk-go-v2を使う場合はドキュメントにある通り、Clientのモックを用意するのが一般的な手法かと思います。
ただテストのためだけにinterfaceを書きたくないので、aws-sdk-go-v2が提供するClientをそのまま使える形にしたいです。
幸いaws-sdk-go-v2はClientをカスタマイズするためのオプションがあるため、大別して以下の2つの方法で実現可能です。
1つ目はAPIリクエストの送信先を変更する方法です。
こちらはWithEndpointResolver
やWithHTTPClient
を用いることで、リクエストをhttptestで立ち上げたサーバーなど、任意の宛先に送信できます。
2つ目はClientの処理に任意の処理を割り込ませる方法です。
各Clientは下図のStack
が実装されており、WithAPIOptionsで
任意の処理を追加できるようになっています。

(詳細はイメージのリンク先へ)
通常はStackを順番に処理していくようになっていますが、途中で次を呼ばずに打ち切ってしまうこともできます。
例えばs3のGetObjectは以下のように呼ぶことでAWSにアクセスせずに"ok"
を返せます。
input := &s3.GetObjectInput{
Bucket: aws.String("bucket"),
Key: aws.String("key"),
}
output, err := client.GetObject(ctx, input, s3.WithAPIOptions(func(stack *middleware.Stack) error {
return stack.Finalize.Add(
middleware.FinalizeMiddlewareFunc("test",
func(context.Context, middleware.FinalizeInput, middleware.FinalizeHandler) (middleware.FinalizeOutput, middleware.Metadata, error) {
return middleware.FinalizeOutput{
Result: &s3.GetObjectOutput{
Body: io.NopCloser(strings.NewReader("ok")),
},
}, middleware.Metadata{}, nil
},
),
middleware.Before,
)
}))
※ s3はFinalizeにリトライ処理があるため、それが呼ばれる前に処理を打ち切ることで数秒のロスを回避できる
しかし、実際のプロダクションコードだと個別のメソッドにオプションを渡すのは難しい形になっているかもしれません。
その際はClientをDIできるようにしておき、config.WithAPIOptionsを使ってClient側にオプションを設定します。
また、WithAPIOptions
はコードがそこそこ大きいのでfunc(*middleware.Stack) error
を返す関数を作成し、応答を渡せるようにしておくと使いやすいです。
以下がテストコードのサンプルです。
package main
import (
"bytes"
"context"
"errors"
"io"
"strings"
"testing"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/smithy-go/middleware"
)
type resp struct {
body string
err error
}
func middlewareForGetObject(r resp) func(*middleware.Stack) error {
return func(stack *middleware.Stack) error {
return stack.Finalize.Add(
middleware.FinalizeMiddlewareFunc(
"test",
func(context.Context, middleware.FinalizeInput, middleware.FinalizeHandler) (middleware.FinalizeOutput, middleware.Metadata, error) {
return middleware.FinalizeOutput{
Result: &s3.GetObjectOutput{
Body: io.NopCloser(strings.NewReader(r.body)),
},
}, middleware.Metadata{}, r.err
},
),
middleware.Before,
)
}
}
func Test_GetObject(t *testing.T) {
type args struct {
bucket string
key string
}
tests := []struct {
name string
args args
resp resp
want []byte
wantErr bool
}{
{
name: "success",
args: args{bucket: "Bucket", key: "Key"},
resp: resp{body: "ok"},
want: []byte("ok"),
},
{
name: "failure",
args: args{bucket: "Bucket", key: "Key"},
resp: resp{err: errors.New("object not found")},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := context.TODO()
cfg, err := config.LoadDefaultConfig(ctx,
config.WithRegion("ap-northeast-1"),
config.WithAPIOptions([]func(*middleware.Stack) error{middlewareForGetObject(tt.resp)}),
)
if err != nil {
t.Fatal(err)
}
client := s3.NewFromConfig(cfg)
out, err := client.GetObject(ctx, &s3.GetObjectInput{Bucket: &tt.args.bucket, Key: &tt.args.key})
if (err != nil) != tt.wantErr {
t.Errorf("GetObject() error = %v, wantErr %v", err, tt.wantErr)
return
}
if tt.wantErr {
return
}
got, _ := io.ReadAll(out.Body)
if !bytes.Equal(tt.want, got) {
t.Errorf("GetObject() = %q, want %q", got, tt.want)
}
})
}
}