moqを触ってみる

# はじめに

Goのテスト用モック生成ライブラリ `moq` を触ってみたので紹介したいと思います。

https://github.com/matryer/moq

モック生成は今まで `gomock` しか使ったことが無かったので、gomockとの違いについても見ていきます。

https://github.com/golang/mock

# バージョン

- moq: `v0.2.7`

# 使ってみる

紹介したコードは以下のリポジトリにまとめています。

https://github.com/Syuparn/moqexample

`go generate` でインターフェースからモックが生成されます。

```go
//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にフォーマットをかけてくれるのでおススメです。

あとは、差し替えたいメソッドの関数オブジェクトをモックに渡すだけです。

```go
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` を呼び出しています。

```go
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()` で取得できるので、引数のテストも可能です。

```go
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` が完了したワークフローを返すまで取得を繰り返しています。

```go
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` が

- 未完了のワークフロー
- 完了したワークフロー

の両方を返す必要があります。

あまり綺麗な方法ではないのですが、クロージャを使うことで毎回異なる戻り値を返すことができました。

```go
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` でテスト可能です。

```go
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](https://speakerdeck.com/uji/moq-shi-tutemita)
[moq - gomockを使わないMock生成 - oinume journal](https://journal.lampetty.net/entry/moq-instead-of-gomock)
[moqを使ってGo言語のテストコードを書く - ペペロミア & memoir開発ブログ](https://www.wheatandcat.me/entry/2021/03/31/221830)