gomockのmockを入力とするmockが意図した出力を返さない理由

golang

interfaceを受け取る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)
	}
}