Open11

gomock と比較される moq,こいつはモックを作るライブラリじゃなくてスタブを作るだけの責務放棄したライブラリや!

mpywmpyw

モックとスタブの違い

https://qiita.com/hirohero/items/3ab63a1cdbe32bbeadf1

↑に書いてある図が全て。

moq の公式マニュアルに書かれている使い方

https://github.com/matryer/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 のほうなのだ。

mpywmpyw

ちなみに gomock のどこかのバージョンから, TB.Cleanup() の採用により,明示的にこれをコールする必要がなくなってました。但しこれを使った場合検証タイミングが「テスト全体が終わってから」になるので,サブテストの失敗結果をリアルタイムに検知したいなら依然として defer ctrl.Finish() は書いたほうが良さそうです。

(とはいっても, go test の出力って,サブテストごとに逐次出てくるんじゃなくて,全部終わってから一気に表示する仕様だったように思うので,とりわけ問題はないのかな…?)

mpywmpyw

端的に言うなら

  • 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 は静的なコード生成により,型安全性を重視したライブラリなのだ。

mpywmpyw

一応こういうインタフェースも別途作ることは出来そうではある。
ただコストに対してメリットがかなり薄いので,わざわざ採用されることはなさそう…

// どちらも受け付ける
client.EXPECT().FetchUser(1)
client.EXPECT().FetchUser(gomock.Eq(1)) 

 // int 型しか受けない
client.EXACT().FetchUser(1)
mpywmpyw

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 と同じことができるはず。
chocochoco

浅慮で恐縮ですが、書籍 テスト駆動開発 には次のような記載があります。

語彙の整理により、xUTP 出版後は「広義の Mock Object」と「狭義の Mock Object」を分けて議論できるようになり、後続のモックライブラリの機能名も明確になりました。
引用:テスト駆動開発 付録 C p290

広義の Mock Object は Test Double を指すので、もしかすると moq は Test Double をモックとして捉えていてモックに対する解釈のズレが生じているのかもなと読んでいて感じました。

以下は参考にした記事です。

mpywmpyw

ありがとうございます!

とはいえど,公式マニュアルのほうにはしっかりアサーションする例が書かれているので,作者自身は狭義の Mock を意識しているのではないかな?とも思いました


と思いましたが, 自分自身で アサーションする機能は備えていないので,狭義のモックの定義は満たしていないかもしれないですね。
(呼出回数アサーションだけ本当に中途半端に持っている形…うーむ)

よく分からずに思いつきで作ったんじゃないかとすら邪推してしまう…


【更に追記】呼び出し回数も,記録しているだけでアサーションしているわけではなかったので, Spy の定義は厳密に満たしていそうです!

mpywmpyw

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 から見てモックが使われていることになる

「モックはスタブの機能を持っていることもある」とされていますが, 間接出力がある場合はその値が別の間接入力として使われることがある ので,持たせざるを得ないんじゃないでしょうかね?

chocochoco

回答が遅くなりました。

間接出力がある場合はその値が別の間接入力として使われることがある

参考記事の翻訳があまり適切でない気がしますね。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 ですね。

mpywmpyw

ありがとうございます!再度確認したところ,呼出回数もアサーションじゃなくてただ記録しているだけでしたね…

狭義の Spy の定義はしっかり満たし,狭義の Mock の定義は全く満たしていない,でスッキリしそうです!