今回は、
- interfaceがinterfaceを返す
- 返り値がメソッドチェーン的に自身を返す
...ような若干めんどくさいタイプのinterfaceをmockすることにする。
mockしたい仕様
package example
import "context"
type SampleRepository interface {
Save(ctx context.Context, sample Sample) error
FindByID(ctx context.Context)
Where() SampleRepositorySearchSpec
}
type SampleRepositorySearchSpec interface {
IDIn(id ...int) SampleRepositorySearchSpec
NameLike(name string) SampleRepositorySearchSpec
Find(ctx context.Context) (SampleList, error)
Get(ctx context.Context) (*Sample, error)
}
type Sample struct {
ID int
UUID string
}
type SampleList []Sample
-
SampleRepository
のWhere()
がSampleRepositorySearchSpec
という新たなinterfaceを返すところがめんどくささの1。 -
SampleRepositorySearchSpec
はFind()
かGet()
が実行されるまで自分自身を返し続けることを求められるのがめんどくささの2です。
github.com/matryer/moq
生成
moq -out mocks_repository.go . SampleRepository SampleRepositorySearchSpec
- 割と自分で書いたときのような見慣れた感じのシンプルなモックを吐く。
- 以下のような感じのをジェネる
type SampleRepositoryMock struct {
// SaveFunc mocks the Save method.
SaveFunc func(ctx context.Context, sample Sample) error
// WhereFunc mocks the Where method.
WhereFunc func() SampleRepositorySearchSpec
// calls tracks calls to the methods.
calls struct {
// Save holds details about calls to the Save method.
Save []struct {
// Ctx is the ctx argument value.
Ctx context.Context
// Sample is the sample argument value.
Sample Sample
}
// Where holds details about calls to the Where method.
Where []struct {
}
}
}
....
mockするときは、メソッド名 + Func
みたいなメソッドが生えるので、
こいつに自由な値を返す関数を設定する、といった感じだ。
使用感
func TestExample(tt *testing.T) {
// mock準備
searcherMock := SampleRepositorySearchSpecMock{
GetFunc: func(ctx context.Context) (sample *Sample, err error) {
return &Sample{
ID: 122,
UUID: "xxxxx",
}, nil
},
FindFunc: func(ctx context.Context) (list SampleList, err error) {
return SampleList{
{
ID: 122,
UUID: "xxxxx",
},
{
ID: 123,
UUID: "xxxxy",
},
}, nil
},
}
searcherMock.IDInFunc = func(id ...int) SampleRepositorySearchSpec {
return &searcherMock
}
repoMock := SampleRepositoryMock{
SaveFunc: func(ctx context.Context, sample Sample) error {
return nil
},
WhereFunc: func() SampleRepositorySearchSpec {
return &searcherMock
},
}
// テスト
tt.Run(`subtest01`, func(t *testing.T) {
sample122, err := repoMock.Where().IDIn(122).Get(context.Background())
assert.NoError(t, err)
assert.Equal(t, 122, sample122.ID)
})
}
SampleRepositorySearchSpec
側のIDIn
や NameLike
メソッドは自身を返さなければならないため、
searcherMock.IDInFunc = func(id ...int) SampleRepositorySearchSpec {
return &searcherMock
}
のように外出ししないといけない。
github.com/golang/mock
mockgen -source ./base.go -package example -destination example.mock.repository.go
割と短いコードが生成される
// Code generated by MockGen. DO NOT EDIT.
// Source: ./base.go
// Package mock_example is a generated GoMock package.
package example
import (
example "bitbucket.org/wanocoltd/vktc/vkproj/videokicks/core/pkg/domain/domain_service/example"
context "context"
gomock "github.com/golang/mock/gomock"
reflect "reflect"
)
// MockSampleRepository is a mock of SampleRepository interface
type MockSampleRepository struct {
ctrl *gomock.Controller
recorder *MockSampleRepositoryMockRecorder
}
// MockSampleRepositoryMockRecorder is the mock recorder for MockSampleRepository
type MockSampleRepositoryMockRecorder struct {
mock *MockSampleRepository
}
// NewMockSampleRepository creates a new mock instance
func NewMockSampleRepository(ctrl *gomock.Controller) *MockSampleRepository {
mock := &MockSampleRepository{ctrl: ctrl}
mock.recorder = &MockSampleRepositoryMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockSampleRepository) EXPECT() *MockSampleRepositoryMockRecorder {
return m.recorder
}
// Save mocks base method
func (m *MockSampleRepository) Save(ctx context.Context, sample example.Sample) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Save", ctx, sample)
ret0, _ := ret[0].(error)
return ret0
}
// Save indicates an expected call of Save
func (mr *MockSampleRepositoryMockRecorder) Save(ctx, sample interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Save", reflect.TypeOf((*MockSampleRepository)(nil).Save), ctx, sample)
}
// Where mocks base method
func (m *MockSampleRepository) Where() example.SampleRepositorySearchSpec {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Where")
ret0, _ := ret[0].(example.SampleRepositorySearchSpec)
return ret0
}
// Where indicates an expected call of Where
func (mr *MockSampleRepositoryMockRecorder) Where() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Where", reflect.TypeOf((*MockSampleRepository)(nil).Where))
}
// MockSampleRepositorySearchSpec is a mock of SampleRepositorySearchSpec interface
type MockSampleRepositorySearchSpec struct {
ctrl *gomock.Controller
recorder *MockSampleRepositorySearchSpecMockRecorder
}
// MockSampleRepositorySearchSpecMockRecorder is the mock recorder for MockSampleRepositorySearchSpec
type MockSampleRepositorySearchSpecMockRecorder struct {
mock *MockSampleRepositorySearchSpec
}
// NewMockSampleRepositorySearchSpec creates a new mock instance
func NewMockSampleRepositorySearchSpec(ctrl *gomock.Controller) *MockSampleRepositorySearchSpec {
mock := &MockSampleRepositorySearchSpec{ctrl: ctrl}
mock.recorder = &MockSampleRepositorySearchSpecMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockSampleRepositorySearchSpec) EXPECT() *MockSampleRepositorySearchSpecMockRecorder {
return m.recorder
}
// IDIn mocks base method
func (m *MockSampleRepositorySearchSpec) IDIn(id ...int) example.SampleRepositorySearchSpec {
m.ctrl.T.Helper()
varargs := []interface{}{}
for _, a := range id {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "IDIn", varargs...)
ret0, _ := ret[0].(example.SampleRepositorySearchSpec)
return ret0
}
// IDIn indicates an expected call of IDIn
func (mr *MockSampleRepositorySearchSpecMockRecorder) IDIn(id ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IDIn", reflect.TypeOf((*MockSampleRepositorySearchSpec)(nil).IDIn), id...)
}
// NameLike mocks base method
func (m *MockSampleRepositorySearchSpec) NameLike(name string) example.SampleRepositorySearchSpec {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "NameLike", name)
ret0, _ := ret[0].(example.SampleRepositorySearchSpec)
return ret0
}
// NameLike indicates an expected call of NameLike
func (mr *MockSampleRepositorySearchSpecMockRecorder) NameLike(name interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NameLike", reflect.TypeOf((*MockSampleRepositorySearchSpec)(nil).NameLike), name)
}
// Find mocks base method
func (m *MockSampleRepositorySearchSpec) Find(ctx context.Context) (example.SampleList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Find", ctx)
ret0, _ := ret[0].(example.SampleList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Find indicates an expected call of Find
func (mr *MockSampleRepositorySearchSpecMockRecorder) Find(ctx interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Find", reflect.TypeOf((*MockSampleRepositorySearchSpec)(nil).Find), ctx)
}
// Get mocks base method
func (m *MockSampleRepositorySearchSpec) Get(ctx context.Context) (*example.Sample, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Get", ctx)
ret0, _ := ret[0].(*example.Sample)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Get indicates an expected call of Get
func (mr *MockSampleRepositorySearchSpecMockRecorder) Get(ctx interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockSampleRepositorySearchSpec)(nil).Get), ctx)
}
使用感
func TestMock2(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
filterMock := NewMockSampleRepositorySearchSpec(ctrl)
filterMock.EXPECT().IDIn(gomock.Any()).Return(filterMock).AnyTimes()
filterMock.EXPECT().NameLike(gomock.Any()).Return(filterMock).AnyTimes()
repoMock := NewMockSampleRepository(ctrl)
repoMock.EXPECT().Where().Return(filterMock)
t.Run(`subtest2`, func(st *testing.T) {
// モックを定義
filterMock.EXPECT().Get(gomock.Any()).Return(&Sample{
ID: 100,
UUID: "xxx",
} , nil)
sample , err := repoMock.Where().IDIn(100).Get(context.Background())
assert.NoError(st , err)
assert.Equal(st , 100 , sample.ID )
})
}
今回のケースでmockしたいのは、究極的には Find(ctx context.Context)
や Get(ctx context.Context)
だけなので、メソッドチェーンの途中経過はどうでもいいのである。
そのあたり、 gomock.Any()
というのが用意されている gomockはなかなか素敵かもしれない。
Expect()...XXX()...Return(好きな返り値)
という形でダミーで返す値を設定するが、引数がinterfaceなため、適切に仕様を満たさない返り値を設定してしまうと、実行時までエラーがわからない という難点はある。
今回はgomockのほうに好感が持てた。