👏

golangのmockとテーブルテストの実装例

2024/12/19に公開

概要

golangのテスト用のモックとテスト実装のはなしです。答えはない。

モックとは

ウェブのバックエンドアプリケーションのテストでは、外部APIなどテスト対象が依存しているリソースのモックを作る場合が多く、モックを作成するツールもたくさんあります。

golang/mock、vektra/mockery、uber-go/mockなど多くのモックツールは、Golangのインタフェースからモック実装を作成するようになっています。

生成されたモックは概ね以下のような使い方になります(vektra/mockeryのドキュメントより)

import (
    "testing"

    "github.com/stretchr/testify/assert"
)

func Test_getFromDB(t *testing.T) {
    mockDB := NewMockDB(t) // モックのオブジェクト作成
    // 期待する(expect)関数の引数と返値を書く
    mockDB.EXPECT().Get("ice cream").Return("chocolate").Once()
    // テスト対象のユースケースとかに渡す
    flavor := getFromDB(mockDB)
    // テスト結果を検証する
    assert.Equal(t, "chocolate", flavor)
}

テスト実装について

golangではよいテストの書き方としてテーブル駆動テストを使用するカルチャーがあります。

前述の例だとテストごとにモックを実体化しているので、テーブル駆動テストの時はテストパターンごとに引数と返値を用意してモックを実体化する必要があります。

テーブルとして用意するパターンが何個かあると思うので下に列挙します。

モックするインタフェースは以下のようなシンプルなものです。

package repo

type SomeResult struct {
	ID   int
	Text string
}

type GetAPI interface {
	Some(query1 string, query2 string) (SomeResult, error)
}

モックを返す関数を持つ

最初に考えたんですがこれはテストケースごとの記述が増えるのでよくないと思います。
何がよくないかというと、テストパターンごとに毎回モック生成するのコードが出てくることですね。
テストテーブルの定義は簡単ではあるかも。モック側で特殊な初期化をしたいパターンがある場合とかもこれでよいのかもしれません。
メモとして書いておきます。

func TestGetAPIUsecase_Do(t *testing.T) {
	type fields struct {
		repo irepo.GetAPI
	}
	type args struct {
		req GetAPIRequest
	}
	tests := []struct {
		name     string
		mockRepo func(t *testing.T) irepo.GetAPI // モックを返す関数を持つ
		args     GetAPIRequest
		want     *GetAPIResponse
		wantErr  bool
	}{
		{
			name: "normal",
			mockRepo: func(t *testing.T) irepo.GetAPI {
				m := repo.NewMockGetAPI(t)
				m.EXPECT().Some("foo", "bar").Return(irepo.SomeResult{
					ID:   1234567890,
					Text: "foo",
				}, nil)
				return m
			},
			args: GetAPIRequest{
				Query1: "foo",
				Query2: "bar",
				Type:   1,
			},
			want: &GetAPIResponse{
				ResultID:   1234567890,
				ResultText: "foo",
				ResultType: 1,
			},
			wantErr: false,
		},
		{
			name: "err",
			mockRepo: func(t *testing.T) irepo.GetAPI {
				m := repo.NewMockGetAPI(t)
				m.EXPECT().Some("bar", "buzz").Return(irepo.SomeResult{
					ID:   256,
					Text: "buzz",
				}, fmt.Errorf("is some error"))
				return m
			},
			args: GetAPIRequest{
				Query1: "bar",
				Query2: "buzz",
				Type:   2,
			},
			want:    nil,
			wantErr: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			u := &GetAPIUsecase{
				repo: tt.mockRepo(t),
			}
			got, err := u.Do(tt.args)
			if (err != nil) != tt.wantErr {
				t.Errorf("GetAPIUsecase.Do() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if !reflect.DeepEqual(got, tt.want) {
				t.Errorf("GetAPIUsecase.Do() = %v, want %v", got, tt.want)
			}
		})
	}
}

モックの引数と返値をテーブルの中に持たせる

こっちのがシンプルですね。
golangに構造体を展開して引数にできるシンタックスシュガーとかあればいいんですけどね。
持たせかたは色々あると思います。シンプルにはこうでしょうか。
モックが増えてきたらさらにネストさせた方が楽かも。

func TestGetAPIUsecase_Do_Flat(t *testing.T) {
	type fields struct {
		repo irepo.GetAPI
	}
	type args struct {
		req GetAPIRequest
	}
	tests := []struct {
		name    string
		mockArg struct {
			arg1 string
			arg2 string
		}
		mockResult struct {
			res irepo.SomeResult
			err error
		}
		args    GetAPIRequest
		want    *GetAPIResponse
		wantErr bool
	}{
		{
			name: "normal",
			mockArg: struct {
				arg1 string
				arg2 string
			}{"foo", "bar"},
			mockResult: struct {
				res irepo.SomeResult
				err error
			}{
				irepo.SomeResult{
					ID:   1234567890,
					Text: "foo",
				}, nil,
			},
			args: GetAPIRequest{
				Query1: "foo",
				Query2: "bar",
				Type:   1,
			},
			want: &GetAPIResponse{
				ResultID:   1234567890,
				ResultText: "foo",
				ResultType: 1,
			},
			wantErr: false,
		},
		{
			name: "err",
			mockArg: struct {
				arg1 string
				arg2 string
			}{"bar", "buzz"},
			mockResult: struct {
				res irepo.SomeResult
				err error
			}{
				irepo.SomeResult{
					ID:   256,
					Text: "buzz",
				}, fmt.Errorf("is some error"),
			},
			args: GetAPIRequest{
				Query1: "bar",
				Query2: "buzz",
				Type:   2,
			},
			want:    nil,
			wantErr: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			m := repo.NewMockGetAPI(t)
			m.EXPECT().Some(tt.mockArg.arg1, tt.mockArg.arg2).Return(irepo.SomeResult{
				ID:   tt.mockResult.res.ID,
				Text: tt.mockResult.res.Text,
			}, tt.mockResult.err)
			u := &GetAPIUsecase{
				repo: m,
			}
			got, err := u.Do(tt.args)
			if (err != nil) != tt.wantErr {
				t.Errorf("GetAPIUsecase.Do() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if !reflect.DeepEqual(got, tt.want) {
				t.Errorf("GetAPIUsecase.Do() = %v, want %v", got, tt.want)
			}
		})
	}
}

上のネストするバージョン

依存するインタフェースが複数あるときはネストして引数・返値の組み合わせを持った方がよいかなと。
型推論がもっと効いてくれると色々書かなくてすむようになるんですけど、現状これが限界そう。

複雑なモックが複数あると読みづらそう。
モックを返す関数のが直接的といえば直接的な書き方のような気もしてきますね。

func TestGetAPIUsecase_Do_Nest(t *testing.T) {
	type fields struct {
		repo irepo.GetAPI
	}
	type args struct {
		req GetAPIRequest
	}
	type mockArg struct {
		arg1 string
		arg2 string
	}
	type mockResult struct {
		res irepo.SomeResult
		err error
	}
	type mockData struct {
		mockArg    mockArg
		mockResult mockResult
	}

	tests := []struct {
		name     string
		mockData mockData
		args     GetAPIRequest
		want     *GetAPIResponse
		wantErr  bool
	}{
		{
			name: "normal",
			mockData: mockData{
				mockArg: mockArg{"foo", "bar"},
				mockResult: mockResult{
					res: irepo.SomeResult{
						ID:   1234567890,
						Text: "foo",
					},
					err: nil,
				}},
			args: GetAPIRequest{
				Query1: "foo",
				Query2: "bar",
				Type:   1,
			},
			want: &GetAPIResponse{
				ResultID:   1234567890,
				ResultText: "foo",
				ResultType: 1,
			},
			wantErr: false,
		},
		{
			name: "err",
			mockData: mockData{
				mockArg: mockArg{"bar", "buzz"},
				mockResult: mockResult{
					res: irepo.SomeResult{
						ID:   256,
						Text: "buzz",
					},
					err: fmt.Errorf("is some error"),
				}},
			args: GetAPIRequest{
				Query1: "bar",
				Query2: "buzz",
				Type:   2,
			},
			want:    nil,
			wantErr: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			m := repo.NewMockGetAPI(t)
			m.EXPECT().Some(tt.mockData.mockArg.arg1, tt.mockData.mockArg.arg2).Return(irepo.SomeResult{
				ID:   tt.mockData.mockResult.res.ID,
				Text: tt.mockData.mockResult.res.Text,
			}, tt.mockData.mockResult.err)
			u := &GetAPIUsecase{
				repo: m,
			}
			got, err := u.Do(tt.args)
			if (err != nil) != tt.wantErr {
				t.Errorf("GetAPIUsecase.Do() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if !reflect.DeepEqual(got, tt.want) {
				t.Errorf("GetAPIUsecase.Do() = %v, want %v", got, tt.want)
			}
		})
	}
}

Discussion