2020-02-10

gomockはともだち こわくないよ!

(キャプ翼は読んだことないです)

今回は gomock を使ったテストコードが読めるようになるために記事としました。 モックの雰囲気を感じ取って貰えれば嬉しいです。

多少癖はありますが、Python の mockと比べればシンプルでわかりやすいと思います。

([python]まだmockで消耗してるの?mockを理解するための3つのポイント)

Sample

最初に、今回モックする対象のプログラムについて簡単に説明します。

world/world.go に、ターゲット を指定するとそのコンテンツの内容を標準出力に表示する Print という関数を作り、 main.go でそれを呼び出しているだけです。

少し複雑なところを挙げるなら、 この Print という関数は抽象化のために Reader というインタフェースを第一引数にとり、第二引数で具体的なターゲットを指定できるようにしました。

(引数としてのインタフェース)

記事の趣旨とは関係ないように見えますが、 gomock はインターフェースを対象にモックを作成するため呼び出し元の関数は インタフェースによって抽象化されている必要があります。

この抽象化により Print は対象がファイルであってもサイトであっても同じように呼び出せるというわけです。

せっかくなら target も構造体のフィールドにしろよという声が聞こえてきそうですが、大人(記事)の事情により引数にしました。

main.go ではサイトのコンテンツを取得するように Site 構造体とURLを指定しているので、 サイトのコンテンツ内容として以下のように表示されるはずです。

$ go run main.go aaaaa

多少の文句はありますが、比較的良いプログラムが書けた気がします。 でもこれで終わりじゃありません。テストがなければ不安ですね。

しかし、いざユニットテストを書こうと思ったときに外部へアクセスする処理は呼び出したくないものです。 今回のプログラムで言えば、 Site.Read は外部へアクセスするメソッドなのでできればテストでは呼び出したくないと思います。

そこで Site.Read はモックすることにしましょう。

Installation

モックするには gomockmockgen のインストールが必要です。

左のようにインストールします。 最初にモジュールモードを有効にしているので右のような go.mod が出来上がります。

  • $ go mod init hello go: creating new go.mod: module golang-mock $ go get github.com/golang/mock/gomock go: finding github.com/golang/mock v1.4.0 go: downloading github.com/golang/mock v1.4.0 go: extracting github.com/golang/mock v1.4.0 $ go get github.com/golang/mock/mockgen go: downloading golang.org/x/tools v0.0.0-20190425150028-36563e24a262 go: extracting golang.org/x/tools v0.0.0-20190425150028-36563e24a262 go: finding golang.org/x/tools v0.0.0-20190425150028-36563e24a262

準備はこれでおわりです。

Mocking

上記でインストールした mockgen コマンドを実行すると -destination オプションに指定したパスに モックのコードが自動生成されます。

# world ディレクトリに移動して world/ $ mockgen -source world.go -destination mock_world/mock_world.go

モックのコードがインタフェース(このプログラムでは Reader)に対して作られていることがわかるでしょうか。

このプログラムを使いテストコードを書いていきます。

関数内の先頭2行はおまじないのようなもので、思考停止ですべてのテストケースに書けばよいです。

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

というと怒られそうなので少し補足すると defer ctrl.Finish() は後処理として (後述する)スタブがすべて呼び出されたかどうかをチェックしています。

Stub

モックは NewMock{インタフェース名} 関数で作ります。 作成したモックに対して どのように振る舞うかを指定するのが EXPECT() 以下のメソッドチェーンです。

info
  • EXPECT()gomock.Call のアドレスを返却し、それ以降のメソッドも同じ gomock.Call のアドレスを返すことでメソッドチェーンを実現しているようです。

この定義された振る舞いが スタブ です

Print はモックを受け取ると内部で m.Read を呼び出すため、定義したスタブが実行されます。

スタブに定義していない引数を指定するとその時点で FAIL になりますし、一度も呼び出されなくてもエラーになります。

  • 定義していない引数を指定した
  • 一度も呼び出さなかった
  • --- FAIL: TestPrint (0.00s) world.go:39: Unexpected call to *mock_world.MockReader.Read([http://note.crohaco.net/not_found.txt]) at golang-mock/world/mock_world/mock_world.go:38 because: expected call at golang-mock/world/world_test.go:18 doesn't match the argument at index 0. Got: http://note.crohaco.net/not_found.txt Want: is equal to http://note.crohaco.net/test.txt panic.go:563: missing call(s) to *mock_world.MockReader.Read(is equal to http://note.crohaco.net/test.txt) golang-mock/world/world_test.go:18 panic.go:563: aborting test due to missing call(s) FAIL exit status 1 FAIL hello/world 0.021s
  • --- FAIL: TestPrint (0.00s) world_test.go:30: missing call(s) to *mock_world.MockReader.Read(is equal to http://note.crohaco.net/test.txt) golang-mock/world/world_test.go:18 world_test.go:30: aborting test due to missing call(s) FAIL exit status 1 FAIL hello/world 0.022s
  • gomock.Any() で引数を定義すれば、どのような引数でも FAIL しません。
  • AnyTimes() をメソッドチェーンに追加すれば呼び出されなくても FAIL しません。

"http://note.crohaco.net/test.txt" が与えられたときの振る舞いとして "bbbbb", "ccccc" を返すようにしているので、テスト実行時にはサイトのコンテンツ(aaaaa)ではなく、 bbbbb, ccccc が順に標準出力に表示されます。

world $ go test bbbbb ccccc PASS ok hello/world 0.030s

ここでは Call.Return しか使っていませんが、他にも呼び出し回数を制限する Call.Times や、呼び出し順を制御する Call.After, gomock.InOrder などもあるので、気になる方はドキュメントを御覧ください。

というわけで前述しましたが gomock を使うにはインタフェースを引数にして関数を書く必要があります。 こういう部分で抽象化を強制(矯正)してくるところに Golang の粋を感じますね。

実務のコードはもう少し呼び出し関係が複雑だと思いますが、やることは概ね同じです。

info

参考

GitHub - golang/mock: GoMock is a mocking framework for the Go programming language.GoMock is a mocking framework for the Go programming language. - GitHub - golang/mock: GoMock is a mocking framework for the Go programming language.https://github.com/golang/mock Go Mockでインタフェースのモックを作ってテストする #golang - QiitaGo Mockとは?https://github.com/golang を漁っていたら,Go Mockというものを見つけました。github上での最初のコミットが2011年なので,かなり昔からあ…https://qiita.com/tenntenn/items/24fc34ec0c31f6474e6d Goでメソッドを簡単にモック化する【gomock】 - QiitaはじめにGoでモックを使ったテストを書く際によく利用されるgomockを触ってみました。gomockとはhttps://github.com/golang/mockテスト用にGoのインターフ…https://qiita.com/gold-kou/items/81562f9142323b364a60