gomockのmockを入力とするmockが意図した出力を返さない理由
golanginterfaceを受け取るinterfaceを定義し、
package main
type Foo interface {
Shout() string
}
type Bar interface {
Bar(foo Foo) string
}
gomockでmockgenする。
// Code generated by MockGen. DO NOT EDIT.
// Source: main.go
// Package main is a generated GoMock package.
package main
import (
reflect "reflect"
gomock "github.com/golang/mock/gomock"
)
...
// MockBarMockRecorder is the mock recorder for MockBar.
type MockBarMockRecorder struct {
mock *MockBar
}
// NewMockBar creates a new mock instance.
func NewMockBar(ctrl *gomock.Controller) *MockBar {
mock := &MockBar{ctrl: ctrl}
mock.recorder = &MockBarMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockBar) EXPECT() *MockBarMockRecorder {
return m.recorder
}
// Bar mocks base method.
func (m *MockBar) Bar(foo Foo) string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Bar", foo)
ret0, _ := ret[0].(string)
return ret0
}
// Bar indicates an expected call of Bar.
func (mr *MockBarMockRecorder) Bar(foo interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Bar", reflect.TypeOf((*MockBar)(nil).Bar), foo)
}
これらを用いて次のようにmockを入力とするmockを作ると、fooMock2を渡しているのに fooMock1の出力が返ってしまう。
package main
import (
"testing"
"github.com/golang/mock/gomock"
)
func TestFoo(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
fooMock1 := NewMockFoo(ctrl)
fooMock2 := NewMockFoo(ctrl)
barMock := NewMockBar(ctrl)
fooMock1.EXPECT().Shout().AnyTimes().Return("1111")
fooMock2.EXPECT().Shout().AnyTimes().Return("2222")
barMock.EXPECT().Bar(fooMock1).AnyTimes().Return("1111")
barMock.EXPECT().Bar(fooMock2).AnyTimes().Return("2222")
if m := barMock.Bar(fooMock2); m != "2222" {
t.Fatalf("expected: 2222, actual: %s\n", m) // => expected: 2222, actual: 1111
}
}
gomockの内部実装を見ると、Controller
のレシーバと関数名をキーとするmap expectedCalls
に呼び出すための情報Call
が追加され、
// https://github.com/golang/mock/blob/v1.6.0/gomock/controller.go#L203
func (ctrl *Controller) RecordCallWithMethodType(receiver interface{}, method string, methodType reflect.Type, args ...interface{}) *Call {
ctrl.T.Helper()
call := newCall(ctrl.T, receiver, method, methodType, args...)
ctrl.mu.Lock()
defer ctrl.mu.Unlock()
ctrl.expectedCalls.Add(call)
return call
}
// https://github.com/golang/mock/blob/v1.6.0/gomock/callset.go#L32
type callSetKey struct {
receiver interface{}
fname string
}
func (cs callSet) Add(call *Call) {
key := callSetKey{call.receiver, call.method}
m := cs.expected
if call.exhausted() {
m = cs.exhausted
}
m[key] = append(m[key], call)
}
呼び出す際はこの中から引数が一致するものを、ConvertしTypeを揃えてreflect.DeepEqualするeqMatcherで探す。
// https://github.com/golang/mock/blob/v1.6.0/gomock/callset.go#L85
for _, call := range exhausted {
if err := call.matches(args); err != nil {
_, _ = fmt.Fprintf(&callsErrors, "\n%v", err)
continue
}
...
}
// https://github.com/golang/mock/blob/v1.6.0/gomock/call.go#L368
for i, m := range c.args {
if !m.Matches(args[i]) {
return fmt.Errorf(
"expected call at %s doesn't match the argument at index %d.\nGot: %v\nWant: %v",
c.origin, i, formatGottenArg(m, args[i]), m,
)
}
}
// https://github.com/golang/mock/blob/v1.6.0/gomock/matchers.go#L110
// Check if types assignable and convert them to common type
x1Val := reflect.ValueOf(e.x)
x2Val := reflect.ValueOf(x)
if x1Val.Type().AssignableTo(x2Val.Type()) {
x1ValConverted := x1Val.Convert(x2Val.Type())
return reflect.DeepEqual(x1ValConverted.Interface(), x2Val.Interface())
}
fooMock1とfooMock2はそれぞれCall
を保存しているわけではなく、同じController
を参照しているためtrue判定され、先に追加されたfooMock1の方で呼ばれることになる。
fmt.Println(reflect.DeepEqual(fooMock1, fooMock2)) // => true
したがって、別のController
で生成すると意図した出力を返すようになる。
package main
import (
"reflect"
"testing"
"github.com/golang/mock/gomock"
)
func TestFoo(t *testing.T) {
ctrl := gomock.NewController(t)
ctrl2 := gomock.NewController(t)
defer ctrl.Finish()
defer ctrl2.Finish()
fooMock1 := NewMockFoo(ctrl)
fooMock2 := NewMockFoo(ctrl2)
barMock := NewMockBar(ctrl)
fooMock1.EXPECT().Shout().AnyTimes().Return("1111")
fooMock2.EXPECT().Shout().AnyTimes().Return("2222")
barMock.EXPECT().Bar(fooMock1).AnyTimes().Return("1111")
barMock.EXPECT().Bar(fooMock2).AnyTimes().Return("2222")
if reflect.DeepEqual(fooMock1, fooMock2) { // ok
t.Fail()
}
if m := barMock.Bar(fooMock2); m != "2222" { // ok
t.Fatalf("expected: 2222, actual: %s\n", m)
}
}
gomock.Any
にして DoAndReturn
で返す方法もある。
package main
import (
"testing"
"github.com/golang/mock/gomock"
)
func TestFoo(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
fooMock1 := NewMockFoo(ctrl)
fooMock2 := NewMockFoo(ctrl)
barMock := NewMockBar(ctrl)
fooMock1.EXPECT().Shout().AnyTimes().Return("1111")
fooMock2.EXPECT().Shout().AnyTimes().Return("2222")
barMock.EXPECT().Bar(gomock.Any()).AnyTimes().DoAndReturn(func(foo Foo) string {
return foo.Shout()
})
if m := barMock.Bar(fooMock2); m != "2222" { // ok
t.Fatalf("expected: 2222, actual: %s\n", m)
}
}
ちなみに、次のようなfuncを引数に取るinterfaceをmockすると
package main
type Bar interface {
Bar(foo func() string) string
}
reflect.DeepEqual
が必ずfalseを返すので
f := func() string { return "foo" }
fmt.Println(reflect.DeepEqual(f, f)) // false
GotとWantが一致していてもmissing callになってしまう。
その場合は、gomock.Any
にするか、独自のMatcherを実装することになる。
package main
import (
"testing"
"github.com/golang/mock/gomock"
)
func TestFoo(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mock := NewMockBar(ctrl)
f := func() string { return "foo" }
mock.EXPECT().Bar(f).Return("bar")
if m := mock.Bar(f); m != "bar" {
// Got: 0x1114620 (func() string)
// Want: is equal to 0x1114620 (func() string)
t.Fatalf("expected: bar, actual: %s\n", m)
}
}