2
Help us understand the problem. What are the problem?

posted at

moqを触ってみる

はじめに

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開発ブログ

Register as a new user and use Qiita more conveniently

  1. You can follow users and tags
  2. you can stock useful information
  3. You can make editorial suggestions for articles
What you can do with signing up
2
Help us understand the problem. What are the problem?