(キャプ翼は読んだことないです)
今回は gomock を使ったテストコードが読めるようになるために記事としました。 モックの雰囲気を感じ取って貰えれば嬉しいです。
多少癖はありますが、Python の mockと比べればシンプルでわかりやすいと思います。
([python]まだmockで消耗してるの?mockを理解するための3つのポイント)
Sample
最初に、今回モックする対象のプログラムについて簡単に説明します。
world/world.go
に、ターゲット
を指定するとそのコンテンツの内容を標準出力に表示する Print
という関数を作り、 main.go でそれを呼び出しているだけです。
-
world.go
package world import ( "fmt" "io/ioutil" "net/http" "os" ) // Reader is an interface type Reader interface { Read(target string) string } // Print is a function func Print(r Reader, target string) { body := r.Read(target) fmt.Println(body) } // File is a struct type File struct{} // Read is a method of File func (f File) Read(target string) string { fd, _ := os.Open(target) defer fd.Close() body, _ := ioutil.ReadAll(fd) return string(body) } // Site is a struct type Site struct{} // Read is a method of Site func (s Site) Read(target string) string { res, _ := http.Get(target) defer res.Body.Close() body, _ := ioutil.ReadAll(res.Body) return string(body) }
-
main.go
package main import ( // カレントディレクトリは go.mod によって hello というモジュール名がつけられている "hello/world" ) func main() { s := world.Site{} world.Print(s, "http://note.crohaco.net/test.txt") }
少し複雑なところを挙げるなら、 この Print という関数は抽象化のために Reader というインタフェースを第一引数にとり、第二引数で具体的なターゲットを指定できるようにしました。
記事の趣旨とは関係ないように見えますが、 gomock はインターフェースを対象にモックを作成するため呼び出し元の関数は インタフェースによって抽象化されている必要があります。
この抽象化により Print は対象がファイルであってもサイトであっても同じように呼び出せるというわけです。
せっかくなら target も構造体のフィールドにしろよという声が聞こえてきそうですが、大人(記事)の事情により引数にしました。
main.go ではサイトのコンテンツを取得するように Site
構造体とURLを指定しているので、
サイトのコンテンツ内容として以下のように表示されるはずです。
$ go run main.go
aaaaa
多少の文句はありますが、比較的良いプログラムが書けた気がします。 でもこれで終わりじゃありません。テストがなければ不安ですね。
しかし、いざユニットテストを書こうと思ったときに外部へアクセスする処理は呼び出したくないものです。
今回のプログラムで言えば、 Site.Read
は外部へアクセスするメソッドなのでできればテストでは呼び出したくないと思います。
そこで Site.Read はモックすることにしましょう。
Installation
モックするには gomock
と mockgen
のインストールが必要です。
左のようにインストールします。
最初にモジュールモードを有効にしているので右のような
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
-
go.mod
module hello go 1.13 require github.com/golang/mock v1.4.0
準備はこれでおわりです。
Mocking
上記でインストールした mockgen
コマンドを実行すると
-destination
オプションに指定したパスに
モックのコードが自動生成されます。
# world ディレクトリに移動して
world/ $ mockgen -source world.go -destination mock_world/mock_world.go
// Code generated by MockGen. DO NOT EDIT.
// Source: world.go
// Package mock_world is a generated GoMock package.
package mock_world
import (
gomock "github.com/golang/mock/gomock"
reflect "reflect"
)
// MockReader is a mock of Reader interface
type MockReader struct {
ctrl *gomock.Controller
recorder *MockReaderMockRecorder
}
// MockReaderMockRecorder is the mock recorder for MockReader
type MockReaderMockRecorder struct {
mock *MockReader
}
// NewMockReader creates a new mock instance
func NewMockReader(ctrl *gomock.Controller) *MockReader {
mock := &MockReader{ctrl: ctrl}
mock.recorder = &MockReaderMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockReader) EXPECT() *MockReaderMockRecorder {
return m.recorder
}
// Read mocks base method
func (m *MockReader) Read(target string) string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", target)
ret0, _ := ret[0].(string)
return ret0
}
// Read indicates an expected call of Read
func (mr *MockReaderMockRecorder) Read(target interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockReader)(nil).Read), target)
}
モックのコードがインタフェース(このプログラムでは
Reader
)に対して作られていることがわかるでしょうか。
このプログラムを使いテストコードを書いていきます。
package world
import (
"testing"
"hello/world/mock_world"
"github.com/golang/mock/gomock"
)
func TestPrint(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
m := mock_world.NewMockReader(ctrl)
m.
EXPECT().
Read("http://note.crohaco.net/test.txt").
Return("bbbbb")
Print(m, "http://note.crohaco.net/test.txt")
m2 := mock_world.NewMockReader(ctrl)
m2.
EXPECT().
Read(gomock.Any()).
Return("ccccc")
Print(m2, "anything is ok")
}
関数内の先頭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
- gomock はモンキーパッチ機能は提供していないので、サードパーティを使う必要があるみたいです。
- https://github.com/bouk/monkey
参考
https://github.com/golang/mock https://qiita.com/tenntenn/items/24fc34ec0c31f6474e6d https://qiita.com/gold-kou/items/81562f9142323b364a60