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が生成したコードとそれを使った場合のコードを見てみる。
- moqが生成したコード client_test.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) }
次に上記のClientMockを使ったコードはこんな感じ。
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と全く同じである。つまり、戻り値の数を間違えてもコンパイル時に検出してくれる。個人的にはこのmoqを使ったコードの方が、boilerplateなコードもなく純粋にモックにフォーカスできるのではないかと思う。
まとめ
gomockではなくmoqを使うとモックを使ったテストコードがよりわかりやすく、かつ使い方を間違えた時の実行時のエラーもなくなることが判ったのではないかと思う。個人的にはモック化する対象が少なければmoqを使わずに、moqが生成するような該当メソッドを置き換えるfuncを手で書いてもいいのではないかと思う。というわけで、gomock割といろんなところで使われているけどより良いツールがあるよ、という紹介でした。おしまい。