gomockでスライスのMatcherを作る
gomockでスライスを順不同で一致させるためにMatcher
を自作しました。
※追記
v1.6.0 から InAnyOrder
というMatcherが用意されているので、不要になりました。
gomock
GolangでUTを書く際に使います。
interfaceの定義からモックを自動生成できるので、比較的簡単にUTが書けます。
例えば以下のようなメソッドがあった時、
func (u *userUsecase) GetByIDs() (Users, error) {
idMap := map[int64]struct{}{
1: {},
2: {},
}
ids := make([]int64, 0, len(idMap))
for id := range idMap {
ids = append(ids, id)
}
us, err := u.userRepository.GetByIDs(ids)
if err != nil {
return nil, err
}
return us, nil
}
userRepository
がinterfaceで定義されていれば、gomockでモック化してテストできます。
しかし、idMap
はmapなのでids
の順番が保証されません。
よって、以下のようなテストを書くと、ランダムでテストが失敗するようになります。
func Test_GetByIDs(t *testing.T) {
...
ctrl := gomock.NewController(t)
repository := mock.NewMockIRepository(ctrl)
repository.EXPECT().
GetByIDs([]int64{1, 2}).
Return([]*user.User{ ... }, nil).
Times(1)
...
}
普通にスライスを渡すと、順番も一致していなければエラーになります。
スライス用のMatcherを定義する
gomockでは任意のMatcher
を定義することができます。
// A Matcher is a representation of a class of values.
// It is used to represent the valid or expected arguments to a mocked method.
type Matcher interface {
// Matches returns whether x is a match.
Matches(x interface{}) bool
// String describes what the matcher matches.
String() string
}
Matcher
をモック関数の引数に渡すと、内部でMatches
メソッドを呼び、true
が返ってきたら一致、とみなすようになっています。
この2つのメソッドを実装します。
type sliceMatcher struct {
slice interface{}
}
func (m sliceMatcher) Matches(x interface{}) bool {
xValue := reflect.ValueOf(x)
mValue := reflect.ValueOf(m.slice)
// 引数がスライスか確認
if xValue.Kind() != reflect.Slice {
return false
}
// 期待値がスライスか確認
if mValue.Kind() != reflect.Slice {
return false
}
// スライスの長さが一致しているか確認
if xValue.Len() != mValue.Len() {
return false
}
mUsedIndex := make(map[int]struct{}, mValue.Len())
for i := 0; i < xValue.Len(); i++ {
x := xValue.Index(i)
match := false
for j := 0; j < mValue.Len(); j++ {
if _, ok := mUsedIndex[j]; ok {
continue
}
m := mValue.Index(j)
// 一致しているものがあるか確認
if reflect.DeepEqual(x.Interface(), m.Interface()) {
match = true
mUsedIndex[j] = struct{}{}
break
}
}
if !match {
return false
}
}
return true
}
func (m sliceMatcher) String() string {
return fmt.Sprintf("is equal to %v", m.slice)
}
var _ gomock.Matcher = sliceMatcher{}
フィールドをinterface{}
で定義しているので、任意の型のスライスをマッチさせることができます。
reflect
パッケージを使い、順不同で一致しているか確認しています。2重ループなのでパフォーマンスは悪いですが、そんなに大きいスライスを扱うことはないと思うので良しとしています。
多分もう少し綺麗に書けると思いますが、テストで使うだけなのでだいぶ妥協してます。
以下のように使います。
func Test_GetByIDs(t *testing.T) {
...
ctrl := gomock.NewController(t)
repository := mock.NewMockIRepository(ctrl)
repository.EXPECT().
GetByIDs(sliceMatcher{[]int64{1, 2}}).
Return([]*user.User{ ... }, nil).
Times(1)
...
}
GetByIDs
に来る値が[]int64{1, 2}
または[]int64{2, 1}
の場合に一致します。
mapを使った処理でも簡単にテストが書けるようになりました。