Go Mockとは?
https://github.com/golang を漁っていたら,Go Mockというものを見つけました。
github上での最初のコミットが2011年なので,かなり昔からあるようです。
名前からして何かのモックを作るライブラリだということは分かります。
READMEを見ると以下のように説明されています。
GoMock is a mocking framework for the Go programming language. It integrates well with Go's built-in testing package, but can be used in other contexts too.
どうやらtesting
パッケージと一緒に使うようです。
インストール
READMEに書いてある通り,インストールしてみましょう。
どうやら,gomock
というパッケージとmockgen
というコマンドラインツールのインストールが必要なようです。
$ go get github.com/golang/mock/gomock
$ go get github.com/golang/mock/mockgen
インタフェースのモックを作る
mockgen
を使うと与えたGoのソースからそのコード中にあるインタフェースのモックを作ってくれるようです。
以下のように,Sample
というインタフェースだけを定義したソースコードsample.go
をmockgen
に与えてみましょう。
package sample
type Sample interface {
Method(s string) int
}
mockgen
は以下のように実行します。
$ mockgen -source sample.go
どうでしょう?標準の出力にGoのソースが表示されてびっくりしたことでしょう。
今度は-destination
をつけてファイルに吐き出しましょう。
$ mkdir -p mock_sample
$ mockgen -source sample.go -destination mock_sample/mock_sample.go
以下のようなファイルがmock_sample
以下に作られたはずです。
// Automatically generated by MockGen. DO NOT EDIT!
// Source: sample.go
package mock_sample
import (
gomock "github.com/golang/mock/gomock"
)
// Mock of Sample interface
type MockSample struct {
ctrl *gomock.Controller
recorder *_MockSampleRecorder
}
// Recorder for MockSample (not exported)
type _MockSampleRecorder struct {
mock *MockSample
}
func NewMockSample(ctrl *gomock.Controller) *MockSample {
mock := &MockSample{ctrl: ctrl}
mock.recorder = &_MockSampleRecorder{mock}
return mock
}
func (_m *MockSample) EXPECT() *_MockSampleRecorder {
return _m.recorder
}
func (_m *MockSample) Method(s string) int {
ret := _m.ctrl.Call(_m, "Method", s)
ret0, _ := ret[0].(int)
return ret0
}
func (_mr *_MockSampleRecorder) Method(arg0 interface{}) *gomock.Call {
return _mr.mock.ctrl.RecordCall(_mr.mock, "Method", arg0)
}
生成されたMockSample
という構造体がSample
インタフェースを実装していることが分かります。
また,_MockSampleRecorder
という構造体も作られています。
名前から察するに,何かを記録しておくものだということが予想できます。
まずは使ってみて,何がおきるのか試してみましょう。
モックを使ってテストを書く
まずは,以下のようなテストファイルsample_test.go
を用意してください。
なお,このファイルはsample.go
と同じディレクトリにおいてください。
package sample
import (
"testing"
"github.com/golang/mock/gomock"
mock "./mock_sample"
)
func TestSample1(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockSample := mock.NewMockSample(ctrl)
mockSample.EXPECT().Method("hoge").Return(1)
t.Log("result:", mockSample.Method("hoge"))
}
つぎに,テストを走らせてみます。-test.v
でログが表示されるようにします。
$ go test -test.v
=== RUN TestSample1
--- PASS: TestSample1 (0.00s)
sample_test.go:18: result 1
PASS
ok
さて,このテストで何を行ったのか,ひとつずつ説明していきます。
最初に,gomcok.NewController(t)
で,gomock.Controller
を作成しています。
defer ctrl.Finish()
で後処理をしているのですが,これがなぜ必要なのかは後述します。
次に,Sample
インタフェースのモックを作っています。
そして,EXPECT().Method()
を呼び出すことで,テスト中に作成したモックのMethod
が呼び出されることを期待します。
EXPECT
メソッドは,mock_sample.go
で宣言されている,_MockSampleRecorder
のポインタを返します。
*_MockSampleRecorder
のメソッドを呼び出すと,*gomock.Controller.Finish
によって後処理されるまでに,対応するモックのメソッドが呼び出されないとエラーになります。
この場合は,*_MockSampleRecorder.Method
メソッドを呼び出してから,*gomock.Controller.Finish
を呼び出すまでに,*MockSample.Method
メソッドを呼びださなければいけません。
つまり,EXPECT
を使えば,モックのメソッドが呼び出されたのかテストすることができます。
EXPECT
を呼び出すと,*gomock.Call
が返って来ます。
Go Mockのドキュメントを見ると,*gomock.Call
はいくつか便利そうなメソッドを持っています。
ここでは,Return
メソッドを使っていますが,このメソッドを使えば,モックのMethod
メソッドを呼び出した時の戻り値を設定できます。
最後にモックのMethod
メソッドを呼び出して,Return
メソッドで設定した1
が返って来ていることが分かります。
複雑なテストを書いてみる
先ほどのサンプルより,もう少し複雑なテストを書いてみましょう。
メソッドの呼び出し順序を指定する
メソッドの呼び出しの順番を指定するには,gomock.InOrder
関数や*gomock.Call.After
メソッドを使用します。
以下のように,呼び出す順番を指定し,この順番にメソッドが実行されない場合はテストが失敗します。
func TestSample2(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockSample := mock.NewMockSample(ctrl)
// 呼び出される順番を指定
gomock.InOrder(
mockSample.EXPECT().Method("hoge").Return(1),
mockSample.EXPECT().Method("fuga").Return(2),
)
/* // 上記と同じ
mockSample.EXPECT().Method("hoge").Return(1).After(
mockSample.EXPECT().Method("fuga").Return(2),
)
*/
t.Log("result", mockSample.Method("hoge"))
t.Log("result", mockSample.Method("fuga"))
}
メソッドの呼び出し回数を指定する
メソッドの呼び出し回数を指定するには,*gomock.Call.Times
メソッドを使用します。
なお,呼び出し回数を指定しない場合は,デフォルトで1回ちょうどだけ呼び出さないとテストが失敗します。
そのため,何回も呼び出しても良い場合は,AnyTimes
メソッドで複数回呼べることを指定します。
func TestSample3(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockSample := mock.NewMockSample(ctrl)
// 呼び出される回数を指定
mockSample.EXPECT().Method("hoge").Return(1).Times(2)
// 何回でも良い場合は,AnyTimes
// mockSample.EXPECT().Method("hoge").Return(1).AnyTimes()
t.Log("result", mockSample.Method("hoge"))
t.Log("result", mockSample.Method("hoge"))
}
既存パッケージのモックを作りたい
残念ながら,Go Mockでは既存パッケージのモックを作成したり,指定したソースコード中にないインタフェースのエイリアス型やインタフェースの埋込みを行った型のモックを作ることができません。
つまり,io.Writer
のモックを作ったり,以下のような型を作ってモックを作ることができません。
// エイリアス型もダメ
type MyWriter1 io.Writer
// 埋込みもダメ
type MyWriter2 interface {
io.Writer
}
しかし,幸いにもGoのインタフェースはダックタイピングを採用しています。
そのため,モックを作りたい既存パッケージのインタフェース(ここではio.Writer
インタフェース)とメソッドリストが全く同じインタフェースをテスト対象の自分のパッケージ中に作ってやれば,モックを作ることができます。
そしてそのインタフェースは,既存パッケージのインタフェースとメソッドリストが全く同じであるため,作ったモックは,自前のインタフェースを実装するとともに,既存パッケージのインタフェースも実装していることになります。
package sample
// io.Writerと同じ
type writer interface {
Write([]byte) (int, error)
}
package sample
import (
"fmt"
"testing"
"github.com/golang/mock/gomock"
mock "./mock_sample"
)
func TestSample(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
// sample.writerとio.Writerを共に実装
w := mock.NewMockwriter(ctrl)
gomock.InOrder(
w.EXPECT().Write([]byte("hoge")).Return(4, nil),
w.EXPECT().Write([]byte("fuga")).Return(4, nil),
)
// io.Writerとして渡す
fmt.Fprintf(w, "hoge")
fmt.Fprintf(w, "fuga")
}
この方法の問題点は,既存パッケージのメソッドリストに変更があった場合に,テストが失敗してしまうことです。
テスト対象のメソッドに変更があった場合に,テストが失敗するのはむしろ好都合のように思えますが,新しいメソッドが追加になった場合は別に失敗する必要はないので困ります。
しかし,Goのインタフェースは,一般的にはあまり大きなメソッドリストは持たず,io.Writer
やio.Reader
のように特定のメソッドに特化したインタフェースを作ることが多いでしょう。
そのため,メソッドの追加や削除が発生することは稀なので,あまり心配することは無いでしょう。
そもそも,業務などでは,外部のパッケージのバージョンを固定して作業することが多いのではないでしょうか。
おわりに
この記事ではGo Mockについて調べ,簡単な使い方について説明しました。
今回使ったソースコードはgithubにあげておくので,ぜひ手元で動かしてみてください。