これはGo6 Advent Calendar 2019の24日目の記事です。
昨日はuchikoさんの「Go言語の依存モジュール管理ツール Modules の使い方」でした。

Goのモックライブラリ gomockで独自のMatcherを作る際の注意点などの備忘。
まずは通常の使い方。
gomock.Any(), gomock.Eq(), gomock.Nil()などいくつかのMatcherが用意されている。
READMEのサンプルを例にすると、
func TestFooEq(t *testing.T) {
    ctrl := gomock.NewController(t)
    defer ctrl.Finish()

    m := NewMockFoo(ctrl)
    m.EXPECT().Bar(1).Return(1)
    SUT(m)
}

func TestFooAny(t *testing.T) {
    ctrl := gomock.NewController(t)
    defer ctrl.Finish()

    m := NewMockFoo(ctrl)
    m.EXPECT().Bar(gomock.Any()).Return(1)
    SUT(m)
}
TestFooEqSUTの中でFoo.Bar()が1を返せばテストが成功し、
TestFooAnyは引数の値が何であれ呼ばれれば成功する。

さて、標準に用意されているもの以外の条件でマッチする事をテストしたいとする。
Matcherインターフェースを満たした struct を独自に作って渡してやればよさそうだ。
試しにこんなMatcherを作ってみた。
type betweenMatcher struct {
	min, max int
}

func (m betweenMatcher) Matches(x interface{}) bool {
	i, ok := x.(int)
	return ok && m.min <= i && m.max >= i
}
早速使ってみよう。
func TestFooBetween(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	m := NewMockFoo(ctrl)
	m.EXPECT().Bar(betweenMatcher{0, 10}).Return(1)
	SUT(m)
}
0から10の間、例えば1を返せばテストは通るはず…だが、失敗してしまった。
--- FAIL: TestFooBetween (0.00s)
    /Users/zaneli/go/src/github.com/zaneli/mocktest/foo.go:8: Unexpected call to *main.MockFoo.Bar([1]) at /Users/zaneli/go/src/github.com/zaneli/mocktest/mocks.go:39 because: 
        Expected call at /Users/zaneli/go/src/github.com/zaneli/mocktest/foo_test.go:42 doesn't match the argument at index 0.
        Got: 1
        Want: is equal to {0 10}
どうも、渡したstructとの完全一致をチェックしているようだ。
よくよく調べてみると、MatcherインターフェースにはMatches()だけでなくString()メソッドも必要な事、
インターフェースを満たしていない値も渡せるが、gomock.Eq()として扱われている事に気づいた。
https://github.com/golang/mock/blob/b4b7d2139f51c2a0de2327eaff590f565c29994f/gomock/call.go#L55-L63
type betweenMatcher struct {
	min, max int
}

func (m betweenMatcher) Matches(x interface{}) bool {
	i, ok := x.(int)
	return ok && m.min <= i && m.max >= i
}

func (m betweenMatcher) String() string {
	return fmt.Sprintf("is between %d and %d", m.min, m.max)
}
String()メソッドも定義する事で無事想定通りテストが通るようになった。
失敗した場合の表示はこのようになる。
--- FAIL: TestFooBetween (0.00s)
/Users/zaneli/go/src/github.com/zaneli/mocktest/foo.go:8: Unexpected call to *main.MockFoo.Bar([11]) at /Users/zaneli/go/src/github.com/zaneli/mocktest/mocks.go:39 because: 
        Expected call at /Users/zaneli/go/src/github.com/zaneli/mocktest/foo_test.go:42 doesn't match the argument at index 0.
        Got: 11
        Want: is between 0 and 10
しかし、もしgomockのバージョンが上がったタイミングなどでMatcherインターフェースのメソッドが変わったりすると、
テスト実行時までそれに気づけず煩わしい。
以下2つの方法で、コンパイル時にチェックする事ができる。
1. 使用しない変数でインターフェースを満たす事だけを宣言だけしておく。
var _ gomock.Matcher = betweenMatcher{}
2. Matcherを返す関数を用意しておく。
func Between(min, max int) gomock.Matcher {
    return betweenMatcher{min, max}
}
今回のような場合は後者のほうが望ましいだろう。
横着せずに最初からgomock.Eq()などと同じようにMatcherを返す関数を書いて、それを使っておくべきだった。
func TestFooBetween(t *testing.T) {
    ctrl := gomock.NewController(t)
    defer ctrl.Finish()

    m := NewMockFoo(ctrl)
    m.EXPECT().Bar(Between(0, 10)).Return(1)
    SUT(m)
}
前者の宣言は積極的に使わなくとも、これがコンパイル時にインターフェースを満たす事を保証するためのもの、と知っておくとコードを読む際に混乱せずに済むと思う。
初めて出くわしたときにはアンダースコアの後に何やら型っぽいのが書かれていて何やってるんだこれ?となった覚えがある。

明日はyukiarrrさんの「【Go】appendで新しく配列が割り当てられる際にどれくらいcapが確保されるか知っていますか?」です。

Copyright© 2011-2021 Shunsuke Otani All Right Reserved .