はじめに
Goのモックの仕方を学ぶために、以下2つの方法を試していきたいと思います。
テスト対象
今回は、以下のfoo.goのFly()
のテストを実装していきたいと思います。
type FourDimensionalPocket interface {
GetTakecopter() (string, error)
GetDokodemodoa() (string, error)
}
type Doraemon struct {
}
func (d Doraemon) GetTakecopter() (string, error) {
return "タケコプター🚁", nil
}
func (d Doraemon) GetDokodemodoa() (string, error) {
return "どこでもドア🚪", nil
}
func NewDoraemon() Doraemon {
return Doraemon{}
}
type Nobita struct {
pocket FourDimensionalPocket
}
type Bird interface {
Fly() string
}
func NewNobita(pocket FourDimensionalPocket) Bird {
return Nobita{
pocket: pocket,
}
}
func (n Nobita) Fly() string {
takecopter, err := n.pocket.GetTakecopter()
if err != nil {
return "飛べません🐧"
}
return fmt.Sprintf("%sを使って飛んでいく!", takecopter)
}
クライアント側で以下のように実行できます。
func main() {
doraemon := NewDoraemon()
nobita := NewNobita(doraemon)
result := nobita.fly()
fmt.Println(result) // タケコプター🚁を使って飛んでいく!
}
このFlyをテストするとき、ライブラリを使わず実装すると、以下のようになります。
ライブラリを使わないモック
package foo_test
import (
"testing"
"foo"
"github.com/stretchr/testify/assert"
)
type mockDoraemon struct {
errFunc string
}
func newMockDoraemon(errFunc string) *mockDoraemon {
return &mockDoraemon{errFunc: errFunc}
}
func (d *mockDoraemon) GetTakecopter() (string, error) {
if d.errFunc == "GetTakecopter" {
return "", assert.AnError
}
return "タケコプター🚁", nil
}
func (d *mockDoraemon) GetDokodemodoa() (string, error) {
if d.errFunc == "GetDokodemodoa" {
return "", assert.AnError
}
return "どこでもドア🚪", nil
}
func TestNobita_fly(t *testing.T) {
tests := []struct {
name string
want string
errFunc string
}{
{
name: "Success Test",
want: "タケコプター🚁を使って飛んでいく!",
errFunc: "",
},
{
name: "Fail Test",
want: "飛べません🐧",
errFunc: "GetTakecopter",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
doraemon := newMockDoraemon(tt.errFunc)
nobita := foo.NewNobita(doraemon)
if got := nobita.Fly(); got != tt.want {
t.Errorf("Nobita.Fly() = %v, want %v", got, tt.want)
}
})
}
}
ここでは、以下のようにnewMockDoraemonと実体を作成しておりますが、
func newMockDoraemon(errFunc string) *mockDoraemon {
return &mockDoraemon{errFunc: errFunc}
}
実際は以下のように、抽象を渡しているので、わざわざ実体を作る必要はありません。
func NewNobita(pocket FourDimensionalPocket) Bird {
return Nobita{
pocket: pocket,
}
}
ゆえに、先ほどのテストは、以下のようにもかけます。
gomock
gomockとはGoogleが開発したモックライブラリ。Googleのリポジトリは、public archiveされており、現在はUberがforkして管理している。
古いリポジトリ : https://github.com/golang/mock
インストール
go install go.uber.org/mock/mockgen@latest
バージョン確認
$ mockgen -version
v0.4.0
モック自動生成
以下のコマンドでモックを自動生成してくれます。
mockgen -source=***.go
先ほどのfoo.go
のモックを作りたい場合は、以下のようにします。
mockgen -source=foo.go -destination=mock/mock_foo.go -exclude_interfaces=Bird
出力先をmock/mock_foo.go
に指定して、Bird
のinterfaceはテストで使用しないので、今回は除外しています。
以下は自動生成したmock_foo.go
です。長いので折りたたみました。
mock/mock_foo.go
// Code generated by MockGen. DO NOT EDIT.
// Source: foo.go
//
// Generated by this command:
//
// mockgen -source=foo.go -destination=mock/mock_foo.go
//
// Package mock_foo is a generated GoMock package.
package mock_foo
import (
reflect "reflect"
gomock "go.uber.org/mock/gomock"
)
// MockFourDimensionalPocket is a mock of FourDimensionalPocket interface.
type MockFourDimensionalPocket struct {
ctrl *gomock.Controller
recorder *MockFourDimensionalPocketMockRecorder
}
// MockFourDimensionalPocketMockRecorder is the mock recorder for MockFourDimensionalPocket.
type MockFourDimensionalPocketMockRecorder struct {
mock *MockFourDimensionalPocket
}
// NewMockFourDimensionalPocket creates a new mock instance.
func NewMockFourDimensionalPocket(ctrl *gomock.Controller) *MockFourDimensionalPocket {
mock := &MockFourDimensionalPocket{ctrl: ctrl}
mock.recorder = &MockFourDimensionalPocketMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockFourDimensionalPocket) EXPECT() *MockFourDimensionalPocketMockRecorder {
return m.recorder
}
// GetDokodemodoa mocks base method.
func (m *MockFourDimensionalPocket) GetDokodemodoa() (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetDokodemodoa")
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetDokodemodoa indicates an expected call of GetDokodemodoa.
func (mr *MockFourDimensionalPocketMockRecorder) GetDokodemodoa() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDokodemodoa", reflect.TypeOf((*MockFourDimensionalPocket)(nil).GetDokodemodoa))
}
// GetTakecopter mocks base method.
func (m *MockFourDimensionalPocket) GetTakecopter() (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetTakecopter")
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetTakecopter indicates an expected call of GetTakecopter.
func (mr *MockFourDimensionalPocketMockRecorder) GetTakecopter() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTakecopter", reflect.TypeOf((*MockFourDimensionalPocket)(nil).GetTakecopter))
}
// MockBird is a mock of Bird interface.
type MockBird struct {
ctrl *gomock.Controller
recorder *MockBirdMockRecorder
}
// MockBirdMockRecorder is the mock recorder for MockBird.
type MockBirdMockRecorder struct {
mock *MockBird
}
// NewMockBird creates a new mock instance.
func NewMockBird(ctrl *gomock.Controller) *MockBird {
mock := &MockBird{ctrl: ctrl}
mock.recorder = &MockBirdMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockBird) EXPECT() *MockBirdMockRecorder {
return m.recorder
}
// Fly mocks base method.
func (m *MockBird) Fly() string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Fly")
ret0, _ := ret[0].(string)
return ret0
}
// Fly indicates an expected call of Fly.
func (mr *MockBirdMockRecorder) Fly() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Fly", reflect.TypeOf((*MockBird)(nil).Fly))
}
テスト自体の実装は以下のようになります。
package foo_test
import (
"testing"
"foo"
mock_foo "foo/mock"
"github.com/stretchr/testify/assert"
gomock "go.uber.org/mock/gomock"
)
func TestNobitaFly(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockPocket := mock_foo.NewMockFourDimensionalPocket(ctrl)
tests := []struct {
name string
mockReturn string
mockError error
expectedFly string
}{
{
name: "正常なケース",
mockReturn: "タケコプター🚁",
mockError: nil,
expectedFly: "タケコプター🚁を使って飛んでいく!",
},
{
name: "エラーケース",
mockReturn: "",
mockError: assert.AnError,
expectedFly: "飛べません🐧",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockPocket.EXPECT().GetTakecopter().Return(tt.mockReturn, tt.mockError).Times(1)
nobita := foo.NewNobita(mockPocket)
assert.Equal(t, tt.expectedFly, nobita.Fly())
})
}
}
gomock v.s. pureなmock
モックしなければならないメソッドが増えると、gomockを使った方が楽そうです。(抽象度が適切かどうかはわかりませんが)
また、自分で実装すると、モックは複雑なので、スタブになることが多いと思います。自分で実装するかgomockを使うかは好みになると思うので、プロジェクトで話し合って決めてください。
その他のモックライブラリ
ほかにもあるが、moqはスタブを作るだけのライブラリらしい。(参考 :gomock と比較される moq,こいつはモックを作るライブラリじゃなくてスタブを作るだけの責務放棄したライブラリや!)