My External Storage

Apr 25, 2021 - 4 minute read - Comments - go

gomockでモックメソッドの引数をいい感じに設定できるcmpmockを作った

gomockgithub.com/golang/mock)のモックメソッドの引数をいい感じに設定できるカスタムマッチャーを作った。



mrepo.EXPECT().Save(ctx, cmpmock.DiffEq(wantUser)).Return(nil)

TL;DR

  • Goのテストでモックを使いたいときのデファクトスタンダードはgomock
  • モックのメソッド引数に期待値を指定するとき諦めたくなる時がある
    • gomock.Any() でごまかして終わりそうになる
  • gomock用にカスタムマッチャーを作った
  • 内部でgoogle/go-cmp を使っている

使い方はこんな感じ。

type UserRepo interface {
  Save(context.Context, *User) error
}

wantUser := &User{}
mrepo := mock.NewMockUserRepo(ctrl)
mrepo.EXPECT().Save(ctx, cmpmock.DiffEq(wantUser)).Return(nil)

Goのテストでモックを使う

Goでモックをテストに使いたいとき、デファクトスタンダードとして使われているのがgomockだろう。 本記事はgomock用にカスタムマッチャーを作った話なのだが、gomock自体の説明は省略する。 使ったことがない方はpkg.go.devを見てみると雰囲気がわかるだろう。

https://pkg.go.dev/github.com/golang/mock@v1.5.0/gomock

モックのメソッド引数に期待値を指定するとき諦めたくなる時がある

gomockは柔軟に挙動を変えたり呼び出し回数や呼び出し順まで検証することができる。
ただ、モックの引数にそこそこでかい構造体を指定したり、時刻情報を用いているとうまく期待値を設置できないときがないだろうか? そういうときはgomock.Any()関数を引数に指定したりして、モックの設定を妥協したりすることがあった。 毎回DoAndReturnなどで構造体をパースする処理をテストコードに書くのもめんどくさい。

また、指定できていたとしても期待値と異なる値がモックメソッドに指定されたとき「どこか期待と異なるのか」が非常にわかりにくい。

たとえば、モックメソッドの引数が次のUser構造体の場合をみてみる、

type User struct {
  Name, Address string
  CreateAt      time.Time
}

期待値と異なった場合、テスト実行時に以下が出力されるのだが、どこに差異があるのかわかるだろうか?

expected call at /Users/budougumi0617/go/src/github.com/budougumi0617/cmpmock/_example/repo_test.go:26 doesn't match the argument at index 1.
Got: &{John Due Tokyo 2021-04-23 02:46:58.145696 +0900 JST m=+0.000595005}
Want: is equal to &{John Due Tokyo 2021-04-23 02:46:48.145646 +0900 JST m=-9.999455563}

gomock用にカスタムマッチャーを作った

gomockのモックメソッドの引数はgomock#Matcherインターフェイスを満たす構造体を定義すれば自分でカスタムマッチャーを用意することができる。 gomock.Any()関数も定義済みgomock#Matcherの一種だ。

使い方は次のように使う。「いい感じに比較してほしい」モックメソッドの期待値を指定するときにcmpmock.DiffEqメソッドでラップして指定すればよい。

type UserRepo interface {
  Save(context.Context, *User) error
}

wantUser := &User{}
mrepo := mock.NewMockUserRepo(ctrl)
mrepo.EXPECT().Save(ctx, cmpmock.DiffEq(wantUser)).Return(nil)

期待結果と異なると、以下のような出力が得られる。

expected call at /Users/budougumi0617/go/src/github.com/budougumi0617/cmpmock/_example/repo_test.go:27 doesn't match the argument at index 1.
Got: &{John Due Tokyo 2021-04-23 02:46:33.290458 +0900 JST m=+0.001035665}
Want: diff(-got +want) is   &_example.User{
  Name:     "John Due",
  Address:  "Tokyo",
-   CreateAt: s"2021-04-23 02:46:33.290458 +0900 JST m=+0.001035665",
+   CreateAt: s"2021-04-23 02:46:23.290383 +0900 JST m=-9.999039004",
}

前述の出力結果と比較してだいぶわかりやすい。

内部でgoogle/go-cmpを使っているだけ

出力結果を見て気づく方はすぐわかるだろうが、DiffEqメソッドは内部でgo-cmpを使って期待値と入力値を比較しているだけだ。
なので正直OSSというより、既存のOSSを組み合わせただけのスニペットである。

挙動はgo-cmpのcmpoptsなどで変更できる

DiffEq関数はシグネチャとしてcmp.Optionを受け取るので、go-cmpを使いなれているひとならば柔軟に比較方法を変更できる。

func DiffEq(v interface{}, opts ...cmp.Option) gomock.Matcher

何も指定しない場合はtime.Timeの差が1秒未満だった場合無視するようにしている。
テストでそこまで厳密な時刻の比較は必要にないと思っている(というより、これが原因で比較を諦めることが大半…)。

func DiffEq(v interface{}, opts ...cmp.Option) gomock.Matcher {
  var lopts cmp.Options
  if len(opts) == 0 {
    lopts = append(lopts, cmpopts.EquateApproxTime(1*time.Second))
  } else {
    lopts = append(lopts, opts...)
  }
  return &diffMatcher{want: v, opts: lopts}
}

終わりに

モックの設定に時間を書けるのは非常にストレスなのでいい感じにモック引数を設定できるDiffEq関数を作った。
既存のOSSを組み合わせただけだが、コスパよく便利なマッチャーを作れた気がする。
これでもっとテストが書けるぞ!!

関連記事