Stub と Mock の違いが分かってきた気がする

mock という言葉だけは知っていたが、実際に mock を作って使ったことはなかった。いよいよ mock を使わないとなぁと思い始めたところで mock は stub じゃないと聞こえてきた。えー。stub ってなんだろう。

今回は RR ( Double Ruby ) の v 1.0.2 を試しながら stub と mock の違いを学んだ話。

btakita/rr - GitHub

stub

例えば RR では

stub( object ).method_name { return_value }

という書き方で object の method を stub 化したものが得られる。戻り値が、ではなく object そのものが stub 化している。

object.method_name      # => return_value
object.method_name( 1 ) # => return_value

この object の .method を呼ぶと規定通りの return_value が返る。引数を指定せずに stub 化したのでどう呼んでも同じ値が返る。1

特定の引数に対して stub を設定する場合は

stub( object ).method_name( arg ) { return_value }

という書き方になる。この場合は arg 以外の引数を与えた場合には動作しない。

ある Klass クラス のオブジェクトすべてを stub 化したい場合は以下のようにする。

stub.instance_of( Klass ).method_name { return_value }

で、これを利用して何らかの動作をするオブジェクトをテストする。stub 化した object を直接テストすることも可能だけど、それはやっても意味ないよね。あくまで stub 化した object を「使う」オブジェクトのテスト用だろう。最もよくあるのは I/O を担うオブジェクトを stub 化することでテストに必要な準備を減らし、テストのスローダウンを避けるといった用途になる。あるいは未実装の機能を呼び出したり外部 API へのアクセスをなくすとか。

スタブは主に生成したりコントロールしたりするのに手間がかかるオブジェクトをもみ消す [stub out] のに使われる。典型的なのはデータベース接続だろう。[手間のかかるオブジェクトを"もみ消す"ので、] 結果としてスタブを見かけるのは、ほとんどの場合、システムの外部との境界や、システム内の複雑なオブジェクトの塊のあたりだったりすることになる。スタブは、実際のオブジェクトと代替できるようインターフェースの実装をし、実際のメソッドをシンプルな [テスト用に] 準備されたデータを使うメソッドで置き換えることで作られる。

ということは stub で覚えてしまおうとしないで stub out で覚えるべきなんだな。

まとめ

RR では

  • 引数を(与えずに|与えて) stub 化
  • 戻り値を(与えずに|与えて) stub 化
  • (クラス|特定のオブジェクト|あるクラスの全インスタンス) の stub 化

を組み合わせて使える。

用途に応じて使い分けるといい。

例えば Rails の Model を stub 化する場合はある id の場合に nil が、ある id の場合にはインスタンスが返るようにしておくとそれっぽく Controller をテストできる。2

mock

mock は stub に似ているが expectation を object に加えることができる。mock を作っておいて呼ばないテストは失敗する。つまり mock は所定のメソッドがちゃんと呼ばれているかどうかをテストするために使う。値も返せるので返る値を使った他のテストも行える。ただし戻り値を利用したテスト3を重視するなら stub を使えということらしい。

stub の場合と同じく RR では

mock( object ).method_name { return_value }

で mock を定義する。上の場合は method は引数を受け取らない。受け取れない。

mock( object ).method( 1 ) { return_value }

の形で定義すれば引数として 1 を受け取ることができるようになる。

イメージとしては mock オブジェクト自身がテスト主体となるような感じか。RR の場合は RR.verify を呼ぶことで、定義した mock が想定通りに実行されていない場合に例外を投げてくれる。

例えば上の定義にしたがって RR.verify を実行する場合、object.method( 1 ) が 2回呼び出されたらエラーになる。何回読んでもよい mock にしたい場合は

mock( object ).method( 1 ).times(any_times) { return_value }

のように記述する。

mock( object ).method(anything).times(any_times) { return_value }

とすれば引数には何でも与えることができる。ただしこの場合は一つ。二つの場合は

mock( object ).method(anything, anything).times(any_times) { return_value }

になる。

また、すべてのインスタンスを mock 化したい場合は

mock.instance_of(Klass).method_name

になるが、この場合もメソッドは1回しか呼ぶことを想定していない形になるので、

mock.instance_of(Klass).method_name.times(any_times)

にするとあらゆるオブジェクトが何回呼ばれても大丈夫になる。

※ ただし1回しか呼ばれないことを想定しているなら、当然この場合は mock としての要件を満たしていない。

mock ライブラリはテスト対象の object を受け取り、mock 自体が、テストしたい振る舞いが object に存在し、正しく呼び出されているかどうかを確認する機能を提供している。(つまり BDD で言う matcher もこの mock ライブラリが用意する。)それ以外の使い方も「一応」できるように return_value の定義もできるが、これは半分おまけのようなものと考えればいいのかな。

感想

mock, mock と言ってきたけど、自分の求めていたものは stub あるいは fake なのだなと思い始めている。少なくとも「呼び出され方」などは今までテスト可能だと思ったことすらなかった。恥ずかしながら。

mock の使いどころはもう少し考えるとしてまずは stub をきちんと使えるようになろうと思う。これは mock の使いどころがまだよく分かっていないためでもあるけど、

RR は明らかに stub 定義の方が簡単

という辺りに、そういう「意図」がこのツールにあると思えるからでもある。

参考

  1. return_value は必ず定義しなければいけないわけではない。その場合は予想通り nil が返る。 

  2. ただし本当にうまく活用するためには scope を使って検索条件をシンプルなメソッドのようにしておいてそれを stub out する、という手間が要る。ここら辺の扱いが上手くなると開発効率はだいぶ高くなるはず。 

  3. これを mock と stub の違いを説明する際には「状態中心のテスト」と呼ぶ 

More