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

Categories

Tool 日々 Web Biz Net Apple MS ことば News Unix howto Food PHP Movie Edu Community Book Security Text TV Perl Ruby Music Pdoc 生き方 RDoc ViewCVS CVS Rsync Disk Mail FreeBSD Cygwin PDF Photo Zebedee Debian OSX Comic Cron Sysadmin Font Analog iCal Sunbird DNS Linux Wiki Emacs Thunderbird Sitecopy Terminal Drawing tDiary AppleScript Life Money Omni PukiWiki Xen XREA Zsh Screen CASL Firefox Fink zsh haXe Ecmascript PATH_INFO SQLite PEAR Lighttpd FastCGI Subversion au prototype.js jsUnit Apache Trac Template Java Rhino Mochikit Feed Bloglines CSS del.icio.us SBS qwikWeb gettext Ajax JSDoc Rails HTML CHM EPWING NDTP EB IE CLI ck ThinkPad Toy WSH RFC readline rlwrap ImageMagick epeg Frenzy sysprep Ubuntu MeCab DTP ERD DBMS eclipse Eclipse Awk RD Diigo XAMPP RubyGems PHPDoc iCab DOM YAML Camino Geekmonkey w3m Scheme Gauche Lisp JSAN Google VMware DSL SLAX Safari Markdown Textile IRC Jabber Fastladder MacPorts LLSpirit CPAN Mozilla Twitter OpenFL Rswatch ITS NTP GUI Pragger Yapra XML Mobile Git Study JSON VirtualBox Samba Pear Growl Mercurial Rack Capistrano Rake Win RSS Mechanize Sitemaps Android JavaScript Python RTM OOo iPod Yahoo Unicode Github iTunes God SBM friendfeed Friendfeed HokuUn Sinatra TDD Test Project Evernote iPad Geohash Location Map Search Simplenote Image WebKit RSpec Phone CSV WiMAX USB Chrome RubyKaigi RubyKaigi2011 Space CoffeeScript Nokogiri Hpricot Rubygems jQuery Node GTD CI UX Design VCS Kanazawa.rb Kindle Amazon Agile Vagrant Chef Windows Composer Dotenv PaaS Itamae SaaS Docker Swagger Grape WebAPI Microservices OmniAuth HTTP 分析基盤 CDN Terraform IaaS HCL Webpack Vue.js BigQuery Middleman CMS AWS PNG Laravel Selenium OAuth OpenAPI GitHub UML GCP TypeScript SQL Hanami Develop Document Jekyll