はじめに
Goのテスト用モック生成ライブラリ moq
を触ってみたので紹介したいと思います。
モック生成は今まで gomock
しか使ったことが無かったので、gomockとの違いについても見ていきます。
バージョン
- moq:
v0.2.7
使ってみる
紹介したコードは以下のリポジトリにまとめています。
go generate
でインターフェースからモックが生成されます。
//go:generate moq -fmt goimports -out employee_moq_test.go . EmployeeRepository
type EmployeeRepository interface {
Get(ctx context.Context, id EmployeeID) (*Employee, error)
}
-fmt goimports
を付けると importにフォーマットをかけてくれるのでおススメです。
あとは、差し替えたいメソッドの関数オブジェクトをモックに渡すだけです。
func TestGetEmployeeUsecase(t *testing.T) {
tests := []struct {
name string
in *GetEmployeeInputData
mockGetEmployee func(context.Context, EmployeeID) (*Employee, error)
expected *GetEmployeeOutputData
}{
{
"get employee Taro",
&GetEmployeeInputData{
EmployeeID: 1234,
},
// メソッドのモック
func(_ context.Context, id EmployeeID) (*Employee, error) {
return &Employee{
ID: EmployeeID(1234),
Name: EmployeeName("Taro"),
}, nil
},
&GetEmployeeOutputData{
&Employee{
ID: EmployeeID(1234),
Name: EmployeeName("Taro"),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
repo := &EmployeeRepositoryMock{
GetFunc: tt.mockGetEmployee, // モック関数を挿す
}
u := &GetEmployeeUsecase{
EmployeeRepository: repo,
}
actual, err := u.Exec(context.TODO(), tt.in)
assert.Equal(t, tt.expected, actual)
assert.Nil(t, err)
})
}
}
モック関数の型がメソッドと同じなので、型を間違えてもコンパイル時に気づくことができます。
仕組み
モックのメソッド Hoge
が、関数オブジェクト HogeFunc
を呼び出しています。
type EmployeeRepositoryMock struct {
// GetFunc mocks the Get method.
GetFunc func(ctx context.Context, id EmployeeID) (*Employee, error)
// ...
}
// Get calls GetFunc.
func (mock *EmployeeRepositoryMock) Get(ctx context.Context, id EmployeeID) (*Employee, error) {
if mock.GetFunc == nil {
panic("EmployeeRepositoryMock.GetFunc: method is nil but EmployeeRepository.Get was just called")
}
// ...
return mock.GetFunc(ctx, id)
}
さらに、呼び出し時に指定した引数一覧は HogeCalls()
で取得できるので、引数のテストも可能です。
func (mock *EmployeeRepositoryMock) Get(ctx context.Context, id EmployeeID) (*Employee, error) {
if mock.GetFunc == nil {
panic("EmployeeRepositoryMock.GetFunc: method is nil but EmployeeRepository.Get was just called")
}
callInfo := struct {
Ctx context.Context
ID EmployeeID
}{
Ctx: ctx,
ID: id,
}
mock.lockGet.Lock()
// 呼ばれるたびに引数リストをappend
mock.calls.Get = append(mock.calls.Get, callInfo)
mock.lockGet.Unlock()
return mock.GetFunc(ctx, id)
}
// GetCalls でGetにこれまで渡された引数を取得可能
func (mock *EmployeeRepositoryMock) GetCalls() []struct {
Ctx context.Context
ID EmployeeID
} {
var calls []struct {
Ctx context.Context
ID EmployeeID
}
mock.lockGet.RLock()
calls = mock.calls.Get
mock.lockGet.RUnlock()
return calls
}
引数リスト読み書きは排他制御されているので、 t.Run
内で並行実行しても安全です。
細かい制御
場合によっては、メソッドの戻り値を変更させる必要もあると思います。
例えば、以下のコードでは、 WorkflowRepository.Get
が完了したワークフローを返すまで取得を繰り返しています。
func (u *CreateEmployeeUsecase) Exec(
ctx context.Context,
in *CreateEmployeeInputData,
) (*CreateEmployeeOutputData, error) {
employee := &employee.Employee{
ID: 1234, // TODO: generate unique number
Name: employee.EmployeeName(in.Name),
}
wf, err := u.EmployeeWorkflowService.Register(ctx, employee)
if err != nil {
return nil, fmt.Errorf("failed to register employee: %w", err)
}
for i := 0; i < 10; i++ {
updatedWF, err := u.WorkflowRepository.Get(ctx, wf.ID) // ここのモックどうする?
if err != nil {
return nil, fmt.Errorf("failed to get workflow: %w", err)
}
if updatedWF.Finished() {
return &CreateEmployeeOutputData{}, nil
}
time.Sleep(1 * time.Second)
}
return nil, fmt.Errorf("workflow timeout: %w", err)
}
このメソッドのテストでは、WorkflowRepository.Get
が
- 未完了のワークフロー
- 完了したワークフロー
の両方を返す必要があります。
あまり綺麗な方法ではないのですが、クロージャを使うことで毎回異なる戻り値を返すことができました。
mockRegisterEmployee: func() func(context.Context, WorkflowID) (*Workflow, error) {
i := 0
return func(_ context.Context, _ WorkflowID) (*Workflow, error) {
wf := []*Workflow{
{ID: 5678, Progress: 20},
{ID: 5678, Progress: 40},
{ID: 5678, Progress: 60},
{ID: 5678, Progress: 80},
{ID: 5678, Progress: 100},
}[i] // n回目呼び出しにはn番目要素を返す
i++
return wf, nil
}
}(),
また、渡された引数のチェックやメソッドが呼ばれた回数は、前述の GetCalls
でテスト可能です。
assert.Equal(t, 5, len(wfRepo.GetCalls()))
for _, c := range wfRepo.GetCalls() {
assert.Equal(t, WorkflowID(5678), c.ID)
}
おわりに
以上、moqを触ってみた紹介でした。型安全でシンプルに書けるので、初心者に優しいと感じました。
一方、引数と戻り値の関係が複雑な場合は準備が少し大変なので、
- 単にダミー値を返すモックが欲しい:
moq
- 振る舞いのテストもしたい:
gomock
と使い分けるのが良さそうです。
参考にさせていただいたサイト
シンプルなモック生成ツール matryer/moq 使ってみた - Speaker Deck
moq - gomockを使わないMock生成 - oinume journal
moqを使ってGo言語のテストコードを書く - ペペロミア & memoir開発ブログ