oinume journal

Scratchpad of what I learned

moq - gomockを使わないMock生成

Goでよく使われるMockの生成ツールとしてgomockがある。個人的にはgomockが生成したコードでモックを書くのが好きではないので、代替としてmoqを使うやり方を取り上げてみようと思う。

題材

GitHubのAPIを使って指定したリポジトリのブランチを出力するServiceを考えてみる。このServiceには以下の引数owner, repoで指定されたリポジトリのブランチをwに出力するというメソッドを持っている。

PrintBranches(ctx context.Context, w io.Writer, owner, repo string) error

service.go

Serviceのコードは以下のようになる。見てもらえばわかるように、githubClientを使ってGitHub APIを呼び出し引数のリポジトリのブランチの一覧を取得している。

github/client.go

次にServiceから参照されているgithub.Client interfaceについて説明する。これは以下のように純粋なinterfaceとして定義している。そして、ユニットテストではGitHub APIへのアクセスをモックにしたいので、このinterfaceが定義しているListBranchesを実装するモックをコメントのgo:generateで生成するようにしている。

package github

//go:generate moq -out=client_test.moq.go . Client
//go:generate mockgen -destination=client_test.gomock.go -package=github . Client

import "context"

type Client interface {
    ListBranches(ctx context.Context, owner, repo string) ([]string, error)
}

gomockを使ったテストコード

それではgomockを使ってService.PrintBranchesのテストコードを書いてみよう。以下のようにgithub/client.go にgo generateでmockgenを呼び出すコードを追加し、go generate ./mock/github を実行すると、./mock/github/client_test.mock.go が生成される。

//go:generate mockgen -destination=client_test.gomock.go -package=github . Client

そして、以下が実際にgomockが生成するMockを使って書いたservice.goのテストコードである(service_test.go)。

ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockGitHubClient := github.NewMockClient(ctrl)
mockGitHubClient.
    EXPECT().
    ListBranches(context.Background(), "oinume", "playground-go").
    Return([]string{"main", "feature/xyz"}, nil)
s := Service{githubClient: mockGitHubClient}
out := new(bytes.Buffer)
if err := s.PrintBranches(context.Background(), out, "oinume", "playground-go"); err != nil {
    t.Fatal(err)
}
fmt.Printf("out = %v\n", out.String())

注目して欲しいのは、以下のEXPECT()以降の部分。

mockGitHubClient.
    EXPECT().
    ListBranches(context.Background(), "oinume", "playground-go").
    Return([]string{"main", "feature/xyz"}, nil)

このEXPECT()が返すのは*MockClientMockRecorder型で、これに定義されているメソッドListBranchesのシグネチャはListBranches(arg0, arg1, arg2 interface{}) *gomock.Callになっている。これは本来のListBranchesのシグネチャとは異なるものになり、引数の型がinterface{}になっているため、オリジナルの引数の型であるstring以外の型も渡せてしまう。テストコードの実行には問題がないかもしれないが、本来であればClient interfaceに定義されている引数の型と同じにしておいた方が変な誤解を生まなくてすむと思う。

また、MockClientMockRecorder.ListBranchesが返す*gomock.Callに対してモックが返す値をReturn([]string{"main", "feature/xyz"}, nil)のようにセットする必要があるが、戻り値の数を間違えてもコンパイル時にはエラーにならず、実行時にエラーになることもイケてない点だ。

また、Mockのために以下のようなboilerplateのコードを毎回書くのも個人的には好きではない。

ctrl := gomock.NewController(t)
defer ctrl.Finish()
...

moqを使ったテストコード

では次に、moqが生成したコードを使った場合のservice_test.goを見てみる。

githubClient := &github.ClientMock{
    ListBranchesFunc: func(ctx context.Context, owner string, repo string) ([]string, error) {
        return []string{"main", "feature/xyz"}, nil
    },
}
s := Service{githubClient: githubClient}
out := new(bytes.Buffer)
if err := s.PrintBranches(context.Background(), out, "oinume", "playground-go"); err != nil {
    t.Fatal(err)
}
fmt.Printf("out = %v\n", out.String())

moqの場合はモックにしたいメソッドをstructの初期化時にセットする。これはただのメソッドなので、中身の実装は好きなように書けばよく、引数の型や戻り値の型も元のClient interfaceのままである。つまり、gomockのように戻り値の数を間違えてもコンパイル時に検出してくれる。個人的にはこのmoqを使ったコードの方が、boilerplateなコードもなく純粋にモックにフォーカスできるのではないかと思う。

まとめ

gomockではなくmoqを使うとモックを使ったテストコードがよりわかりやすく、かつ使い方を間違えた時の実行時のエラーもなくなることが判ったのではないかと思う。個人的にはモック化する対象が少なければmoqを使わずに、moqが生成するような該当メソッドを置き換えるfuncを手で書いてもいいのではないかと思う。というわけで、gomock割といろんなところで使われているけどより良いツールがあるよ、という紹介でした。おしまい。