Goでよく使われるMockの生成ツールとしてgomockがある1。個人的にはgomockが生成したコードでモックを書くのが好きではないので、代替としてmoqを使うやり方を取り上げてみようと思う。
なお、本記事ではgithub.com/golang/mockではなく、go.uber.org/mockを使うように 2024-06-11 に改訂している。[^1]
TL;DR
- moqは、mockしたいinterfaceで定義されているメソッドと同じシグネチャのメソッドを生成し、そのメソッドを実装することでmockが作れる
- 純粋にメソッドのモック実装のコードを書けばいいだけなので、gomockのようにライブラリとしての使い方を覚える必要はない
- 生成されるコードもtype safeであり、gomockのように
any
は登場しない
- moqには、mock化したメソッドが呼び出された回数を取得するなど、最低限のことはできるようになっている。
- それ以上のことをやりたければ自分で実装する
題材
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.moq.go . Client //go:generate mockgen -destination=client.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.mock.go が生成される。
//go:generate mockgen -destination=client.gomock.go -package=github . Client
そして、以下が実際にgomockが生成するMockを使って書いたservice.goのテストコードである(service_test.go)。
ctrl := gomock.NewController(t) defer ctrl.Finish() githubClient := github.NewMockClient(ctrl) githubClient. EXPECT(). ListBranches(context.Background(), "oinume", "playground-go"). Return([]string{"main", "develop", "feature/a"}, 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 ---\n%v\n", out.String())
注目して欲しいのは、以下のEXPECT()
以降の部分。
mockGitHubClient. EXPECT(). ListBranches(context.Background(), "oinume", "playground-go"). Return([]string{"main", "develop", "feature/a"}, nil)
このEXPECT()が返すのは*MockClientMockRecorder
型で、これに定義されているメソッドListBranchesのシグネチャはListBranches(arg0, arg1, arg2 interface{}) *gomock.Call
になっている。これは本来のListBranchesのシグネチャとは異なるものになり、引数の型がinterface{}
になっているため、オリジナルの引数の型であるstring
以外の型も渡せてしまう。テストコードの実行には問題がないかもしれないが、本来であればClient interfaceに定義されている引数の型と同じにしておいた方が変な誤解を生まなくてすむと思う。
また、MockClientMockRecorder.ListBranchesが返す*gomock.Call
に対してモックが返す値をReturn([]string{"main", "develop", "feature/a"}, nil)
のようにセットする必要があるが、戻り値の数を間違えてもコンパイル時にはエラーにならず、実行時にエラーになることもイケてないと思う。
また、Mockのために以下のようなboilerplateのコードを毎回書くのも個人的には好きではない。俺はメソッドをmockしたいだけなのになぜこんなことをしなければならないのか
ctrl := gomock.NewController(t)
defer ctrl.Finish()
...
moqを使ったテストコード
では次に、moqが生成したコードとそれを使った場合のコードを見てみる。
- moqが生成したコード client.moq.go
- moqを使うservice_test.go
まずmoqが生成したコードは以下のようになっている。
type ClientMock struct { // ListBranchesFunc mocks the ListBranches method. ListBranchesFunc func(ctx context.Context, owner string, repo string) ([]string, error) // calls tracks calls to the methods. calls struct { // ListBranches holds details about calls to the ListBranches method. ListBranches []struct { // Ctx is the ctx argument value. Ctx context.Context // Owner is the owner argument value. Owner string // Repo is the repo argument value. Repo string } } lockListBranches sync.RWMutex } // ListBranches calls ListBranchesFunc. func (mock *ClientMock) ListBranches(ctx context.Context, owner string, repo string) ([]string, error) { if mock.ListBranchesFunc == nil { panic("ClientMock.ListBranchesFunc: method is nil but Client.ListBranches was just called") } callInfo := struct { Ctx context.Context Owner string Repo string }{ Ctx: ctx, Owner: owner, Repo: repo, } mock.lockListBranches.Lock() mock.calls.ListBranches = append(mock.calls.ListBranches, callInfo) mock.lockListBranches.Unlock() return mock.ListBranchesFunc(ctx, owner, repo) }
そして、上記のmoqが生成したClientMockを使うコードはこんな感じになる。
githubClient := &github.ClientMock{ ListBranchesFunc: func(ctx context.Context, owner string, repo string) ([]string, error) { return []string{"main", "develop", "feature/a"}, 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 ---\n%v\n", out.String())
moqの場合はモックしたいメソッドをstructの初期化時にセットする。これはただのメソッドなので、中身の実装は好きなように書けばよくて、引数の型や戻り値の型も元のClient interfaceと全く同じである。つまり、戻り値の数を間違えてもコンパイル時に検出してくれる。このmoqを使ったコードの方がboilerplateなコードもなく純粋にモックにフォーカスできるのではないかと思う。
まとめ
gomockではなくmoqを使うとモックを使ったテストコードがよりわかりやすく、かつ使い方を間違えた時の実行時のエラーもなくなることが伝わったのではないかと思う。個人的にはモック化する対象のメソッドが少なければ、moqを使わずにmoqが生成するような該当メソッドを置き換えるfuncを手で書くのもありだと思う。というわけで、gomock割といろんなところで使われているけどより良いツールがあるよ、という紹介でした。おしまい。
- 2023年1月時点で github.com/golang/mock がアーカイブされたため、go.uber.org/mock としてforkされたものが継続的に開発されているため↩