go言語のコンストラクタでinterfaceを返す
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/gomock
go 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 interface
type 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を介してインタラクションするとオブジェクト間の結合度をある程度低減しつつ、拡張性を向上することの副作用としてモックが利用できます。