aws-sdk-go-v2をモックせずにテストする

テストでaws-sdk-go-v2を使う場合はドキュメントにある通り、Clientのモックを用意するのが一般的な手法かと思います。
ただテストのためだけにinterfaceを書きたくないので、aws-sdk-go-v2が提供するClientをそのまま使える形にしたいです。

幸いaws-sdk-go-v2はClientをカスタマイズするためのオプションがあるため、大別して以下の2つの方法で実現可能です。

1つ目はAPIリクエストの送信先を変更する方法です。
こちらはWithEndpointResolverWithHTTPClientを用いることで、リクエストをhttptestで立ち上げたサーバーなど、任意の宛先に送信できます。

2つ目はClientの処理に任意の処理を割り込ませる方法です。
各Clientは下図のStackが実装されており、WithAPIOptionsで任意の処理を追加できるようになっています。 middleware
(詳細はイメージのリンク先へ)

通常は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)
            }
        })
    }
}