mock という言葉だけは知っていたが、実際に mock を作って使ったことはなかった。いよいよ mock を使わないとなぁと思い始めたところで mock は stub じゃないと聞こえてきた。えー。stub ってなんだろう。
今回は RR ( Double Ruby ) の v 1.0.2 を試しながら stub と mock の違いを学んだ話。
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 定義の方が簡単
という辺りに、そういう「意図」がこのツールにあると思えるからでもある。
参考
- brian - Introducing RR
- Mock, Stub 定義文法の比較と RR の紹介
- Ruby stubbing and mocking with rr at technical.pickles
- これ分かりやすかった
- btakita/rr - GitHub
- stub, mock オブジェクトを定義しテストできるもの