golangのmockとテーブルテストの実装例
概要
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