go 言語において呼び出し元に interface のみを公開することで実装の詳細を隠ぺいすると、gomock を使ったテストがやり易くなります。
ざっくり結論だけ書くとこんな感じのコーディングパターンにすると良いよって感じなので、これだけ見て、何が言いたいのかすぐに分かる人は続きを読まなくても良いと思います。
package intf
type Duck interface { Quack() string}
type duck struct {}
func NewDuck() Duck { return &duck{}}
func (d *duck) Quack() string { return "QUUAAAACCCCKK!!!"}
interface と実装が分離されたアヒル#
最初のポイントは、公開する interface として定義された Duck と非公開の struct として定義された duck があることです。
これによって Duck の利用者から実装の詳細を隠ぺいします。
具体的には、 NewDuck 関数でのみ duck のメモリを確保し、interface である Duck 型として利用者に公開します。
利用者側のコードを確認します。
package intf_test
import ( . "." "testing")
func TestQuack(t *testing.T) { duck := NewDuck() if x := duck.Quack(); len(x) < 1 { t.Errorf("duck doesn't quack") }}
go 言語ではそれほどしつこく型について記述しなくても動作するので、NewDuck 関数の戻り値の型が Duck か duck なのかが、あまり気になりません。
というわけで、特に嬉しさが分かりませんね。
interface とのインタラクション#
それでは Duck とインタラクションのあるクラスのようなものを書いてみましょう。
package intf
import ( "bytes" "fmt")
type Farmer interface { Breed() string}
type farmer struct { ducks []Duck}
func NewFarmer(ducks ...Duck) Farmer { return &farmer{ducks}}
func (f *farmer) Breed() string { var b bytes.Buffer for i, d := range f.ducks { fmt.Fprintf(&b, "#%d %s\n", i, d.Quack()) } return b.String()}
農夫がアヒルを鳴かせていますね。
farmer は処理を Duck に依存しています。 Duck は interface なので好きな実装型を farmer に設定できます。
自らの名を哭くアヒル#
自らの名前を鳴くアヒルを作ります。
package intf
type selfCallingDuck struct { name string}
func NewNamedDuck(name string) Duck { return &selfCallingDuck{name}}
func (d *selfCallingDuck) Quack() string { return d.name}
これで、実装の異なる二種類のアヒルを農夫が鳴かせるようにします。
package main
import ( "encapsulation/intf" "fmt")
func main() { duck := intf.NewDuck() john := intf.NewNamedDuck("john") farmer := intf.NewFarmer(duck, john) fmt.Println(farmer.Breed())}
実行するとこうなります。
##0 QUUAAAACCCCKK!!!##1 john
次は、farmer が依存する Duck をモックにしてみましょう。
gomock を使ってテストする#
go 言語でテスト用のモックオブジェクトを利用したいときは、gomockを使います。
それでは、gomock をインストールします。
go get code.google.com/p/gomockgo install code.google.com/p/gomock/mockgen
次に、Duck を実装するモックを生成します。
mockgen -source duck.go -package intf_test > duck_mock_test.go
生成されたモックのコードは以下の様になります。
// Automatically generated by MockGen. DO NOT EDIT!// Source: duck.go
package intf_test
import ( gomock "code.google.com/p/gomock/gomock")
// Mock of Duck interfacetype MockDuck struct { ctrl *gomock.Controller recorder *_MockDuckRecorder}
// Recorder for MockDuck (not exported)type _MockDuckRecorder struct { mock *MockDuck}
func NewMockDuck(ctrl *gomock.Controller) *MockDuck { mock := &MockDuck{ctrl: ctrl} mock.recorder = &_MockDuckRecorder{mock} return mock}
func (_m *MockDuck) EXPECT() *_MockDuckRecorder { return _m.recorder}
func (_m *MockDuck) Quack() string { ret := _m.ctrl.Call(_m, "Quack") ret0, _ := ret[0].(string) return ret0}
func (_mr *_MockDuckRecorder) Quack() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Quack")}
これを使って、farmer をテストしてみましょう。
package intf_test
import ( . "." "code.google.com/p/gomock/gomock" "fmt" "testing")
func TestBreed(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish()
duck := NewMockDuck(ctrl) quack := "quack quack" duck.EXPECT().Quack().Return(quack)
farmer := NewFarmer(duck) fmt.Println(farmer) breed := farmer.Breed()
if fmt.Sprintf("#0 %s\n", quack) != breed { t.Error("breeding is failed [" + breed + "]") }}
EXPECT 関数で返ってくるオブジェクトのメソッドを呼び出して、モックをセットアップしている部分が重要です。
セットアップしたモックのメソッドが最終的に定義された順序で定義された回数利用されたかは、Controller の Finish 関数内で評価されます。
まとめ#
モックを使ったテストは比較的壊れやすい部類のテストである為、 大きなコストをかけて行うべきかは議論の余地があるかと思いますが、
各オブジェクトに interface を定義した上で、モジュールが interface を介してインタラクションするとオブジェクト間の結合度をある程度低減しつつ、拡張性を向上することの副作用としてモックが利用できます。