gomock と比較される moq,こいつはモックを作るライブラリじゃなくてスタブを作るだけの責務放棄したライブラリや!
発端
モックとスタブの違い
↑に書いてある図が全て。
moq
の公式マニュアルに書かれている使い方
func TestCompleteSignup(t *testing.T) {
var sentTo string
mockedEmailSender = &EmailSenderMock{
SendFunc: func(to, subject, body string) error {
sentTo = to // ← 引数アサーション
return nil
},
}
CompleteSignUp("me@email.com", mockedEmailSender)
callsToSend := len(mockedEmailSender.SendCalls())
if callsToSend != 1 {
t.Errorf("Send was called %d times", callsToSend)
}
// ↓引数アサーション
if sentTo != "me@email.com" {
t.Errorf("unexpected recipient: %s", sentTo)
}
}
モックは受け取る引数を検証する ためのものであり,何を返すかはテスト側で副次的に決めるだけなので本質はそっちじゃない。なので上記の 引数アサーション
を放棄したものはモックと呼んではならない。それはただのスタブだ。
もとのブログで主張されている使い方
githubClient := &github.ClientMock{
ListBranchesFunc: func(ctx context.Context, owner string, repo string) ([]string, error) {
return []string{"main", "feature/xyz"}, nil // ← 引数アサーションを書いていない
},
}
s := Service{githubClient: githubClient}
out := new(bytes.Buffer)
if err := s.PrintBranches(context.Background(), out, "oinume", "playground-go"); err != nil {
t.Fatal(err)
}
fmt.Printf("out = %v\n", out.String())
// ↓引数アサーションも呼び出し回数アサーションも書いていない
はっきり申し上げる。これはスタブだ。ゆえに,モックライブラリとしてこれら 2 つを比較するなら不公平として見ていいと思う。
defer ctrl.Finish()
これに関しても, 呼出回数アサーションを任せるタイミングを「テスト関数を抜けるとき」で gomock に任せるための記述 であり,一方の moq は明示的に .SendCalls()
の返り値のアサーションを書く必要がある。むしろ手抜きできてラクなのは gomock のほうなのだ。
ちなみに gomock のどこかのバージョンから, TB.Cleanup()
の採用により,明示的にこれをコールする必要がなくなってました。但しこれを使った場合検証タイミングが「テスト全体が終わってから」になるので,サブテストの失敗結果をリアルタイムに検知したいなら依然として defer ctrl.Finish()
は書いたほうが良さそうです。
(とはいっても, go test
の出力って,サブテストごとに逐次出てくるんじゃなくて,全部終わってから一気に表示する仕様だったように思うので,とりわけ問題はないのかな…?)
端的に言うなら
- gomock は型安全なモック処理を行うための正統派ライブラリで,スタブのためにも使える
- moq はスタブを手軽に作るために特化したライブラリで,モック機能はおまけ
gomock が any
で受けている部分があるから型安全じゃないとブログでは主張されているが,これは意図的なデザインである。引数で受けたものを検証するために Matcher
という仕組みが用意されている。直接値を受け取る場合と, Matcher
でラップしたものを受け取る場合があるのだ。直接受けた値 v
は,内部的に gomock.Eq(v)
としてラップされ,全て Matcher
に統一される。 以下の 2 つの記述は等価だ。
client.EXPECT().FetchUser(1)
client.EXPECT().FetchUser(gomock.Eq(1))
値の完全一致を調べるgomock.Eq()
は非常によく使われる Matcher
であるため,そのまま値を渡せるようになっているのだ。こういう仕様なら型宣言が any
になるのはやむを得ないし,むしろ型安全なほうに倒すなら型制約は Matcher
のみになるべきだ。 ここで int
を要求してしまうと,値の完全一致以外の比較ができなくなってしまい,柔軟性に欠ける。
一方で,期待を定義する client.EXPECT().FetchUser()
ではなく, 実際に動かしてもらう client.FetchUser()
のほうは本来の型通りになっている。何も問題はなく,むしろ gomock は静的なコード生成により,型安全性を重視したライブラリなのだ。
一応こういうインタフェースも別途作ることは出来そうではある。
ただコストに対してメリットがかなり薄いので,わざわざ採用されることはなさそう…
// どちらも受け付ける
client.EXPECT().FetchUser(1)
client.EXPECT().FetchUser(gomock.Eq(1))
// int 型しか受けない
client.EXACT().FetchUser(1)
moq でモック的に使いたい
マニュアルにも書かれている通り,面倒なコードを書く必要がある。全く Easy じゃなく,むしろ Simple を追求した形だ。
gomock でスタブ的に使いたい
どうやるんだろう?と思った方がおられるかもしれないが, gomock はこれにもちゃんと対応してくれる。 EXPECT()
に続けて期待を書いたあと,以下のように繋げられる。
-
Return()
は指定された値を返す Do()
は何か任意のコードを実行するDoAndReturn()
は何か任意のコードを実行してその結果を返す
もとのブログにある moq によるスタブの例を gomock の例に置き換えてみる。
githubClient := &github.NewMockClient()
githubClient
EXPECT().
ListBranches(gomock.Any(), gomock.Any(), gomock.Any()).
Return([]string{"main", "feature/xyz"}, nil)
githubClient := &github.NewMockClient()
githubClient
EXPECT().
ListBranches(gomock.Any(), gomock.Any(), gomock.Any()).
DoAndReturn(func (ctx context.Context, owner string, repo string) ([]string, error) {
return []string{"main", "feature/xyz"}, nil
})
ポイント
- スタブを作るだけであれば,引数のところは全て
gomock.Any()
によるMatcher
でよい。検証の必要がないからだ。 - シンプルに値を返すだけでよければ
Return()
でよい。 - 何か動かした結果の値を返したければ
DoAndReturn()
を使う。こちらを使えば, moq と同じことができるはず。
浅慮で恐縮ですが、書籍 テスト駆動開発 には次のような記載があります。
語彙の整理により、xUTP 出版後は「広義の Mock Object」と「狭義の Mock Object」を分けて議論できるようになり、後続のモックライブラリの機能名も明確になりました。
引用:テスト駆動開発 付録 C p290
広義の Mock Object は Test Double を指すので、もしかすると moq は Test Double をモックとして捉えていてモックに対する解釈のズレが生じているのかもなと読んでいて感じました。
以下は参考にした記事です。
ありがとうございます!
とはいえど,公式マニュアルのほうにはしっかりアサーションする例が書かれているので,作者自身は狭義の Mock を意識しているのではないかな?とも思いました
と思いましたが, 自分自身で アサーションする機能は備えていないので,狭義のモックの定義は満たしていないかもしれないですね。
(呼出回数アサーションだけ本当に中途半端に持っている形…うーむ)
よく分からずに思いつきで作ったんじゃないかとすら邪推してしまう…
【更に追記】呼び出し回数も,記録しているだけでアサーションしているわけではなかったので, Spy の定義は厳密に満たしていそうです!
xUnit Test PatternsのTest Doubleパターン(Mock、Stub、Fake、Dummy等の定義) - 千里霧中
↑を読みました。
Dummy | Fake | Stub | Spy | Mock | |
---|---|---|---|---|---|
テスト対象から渡される間接入力 およびその回数のアサーション |
✅ | ||||
テスト対象から渡される間接入力 およびその回数の記録 |
✅ | ||||
テスト対象へ渡す間接入力の生成 | ✅ | ✅ (返り値ありの場合) |
|||
実際に近い動作 (接続先のみを変えるなど) |
✅ |
Stub だけちょっとわかりにくい部分がありますね
type UserFetcher interface {
FetchUser(userId int) (User, error)
}
type UserUpdater interface {
UpdateUser(user User) error
}
func (*u UserUsecase) UpdateName(userId int, newName string) (User, error) {
user, err := u.userFetcher.FetchUser(userId)
if err != nil {
return user, err
}
user.Name = newName
if err := u.userUpdater.UpdateUser(user); err != nil {
return user, err
}
return user, nil
}
上記の例で考えると,以下のようになるでしょうか?
-
UserFetcher.FetchUser
への間接入力をアサーションすることは,UserUsecase.UpdateName
から見てモックが使われていることになる -
UserFetcher.FetchUser
からの間接出力を定義することは,次にそれを間接入力として受けるUserUpdater.UpdateUser
および後続のロジックで参照するUserUsecase.UpdateName
から見てスタブが使われていることになる -
UserUpdater.UpdateUser
への間接入力をアサーションすることは,UserUsecase.UpdateName
から見てモックが使われていることになる
「モックはスタブの機能を持っていることもある」とされていますが, 間接出力がある場合はその値が別の間接入力として使われることがある ので,持たせざるを得ないんじゃないでしょうかね?
回答が遅くなりました。
間接出力がある場合はその値が別の間接入力として使われることがある
参考記事の翻訳があまり適切でない気がしますね。xUTP のページを見つけたので本文を読んでみると、
Typically, the Mock Object also includes the functionality of a Test Stub in that it must return values to the SUT
http://xunitpatterns.com/Test Double.html Variation: Mock Object より引用
とあるのでスタブの機能を持つ必要があるように読めます。
改めて moq のコード例を見ると、テストコード側でアサーションを実行しているのでスパイの振る舞いだなと思いました。狭義の Mock Object であればアサーションは Mock Object 内で行われていてテストコード側でのアサーションは不要なはずなので。gomock の Building Mocks を見ましたが正に狭義の Mock Object ですね。
ありがとうございます!再度確認したところ,呼出回数もアサーションじゃなくてただ記録しているだけでしたね…
狭義の Spy の定義はしっかり満たし,狭義の Mock の定義は全く満たしていない,でスッキリしそうです!