より「普通に」書くためのTest Doubleライブラリ「crispy」
by Yuji Yamamoto on December 5, 2014
この記事は、ソフトウェアテストあどべんとかれんだー2014と、
Ruby Advent Calendar 2014の12月5日の記事を兼ねています。
前日の記事はそれぞれ以下のものでした。
- ソフトウェアテストあどべんとかれんだー2014がsue445さんの「Paraductでお手軽マトリクステスト #SWTestAdvent」で、
- Ruby Advent Calendar 2014がtbpgrさんの「条件分岐とループベースのロジックからコレクションパイプラインを利用したロジックへ #ruby」でした。
RubyのAdvent CalendarとテストのAdvent Calendarということで、 今日は私が半年ぐらい前からちまちま作っている、crispyというTest Doubleのためのgemを紹介させてください。
crispyって?
「Test Double」がそもそも何なのかはちょっと今回は割愛させていただきます。
Wikipediaなりよそで検索するなりしてください。
一言で言うとstubとかmockとかtest spyとかの総称です。
RSpecをお使いの方であれば知らず知らずのうちに
.to receive(:foo).and_return(:bar) allow(hoge)
とか
.to receive(:foo) expect(hoge)
とか、あるいは古いsyntaxなら
.stub(:foo).and_return(:bar)
hoge
.should_receive(:foo) hoge
という機能を使ったことがある、なんて方もいらっしゃるかもしれません。 これはRSpecデフォルトのTest Doubleライブラリ、「rspec-mocks」です。
crispyはこのrspec-mocksを始め、rr、flexmockなど、
Ruby界に並み居る競合Test Doubleライブラリの置き換えになるものを目指して作られています。
今のバージョンは0.2.0ですが、主要な機能は十分に使えるものとなっていますので、
よろしければREADMEを読んでお試しください。
何が違うの?
crispyはテストをより「普通の」順番で、あるいは「普通の」Rubyで書くことを可能にするTest Doubleライブラリです。
これまでのTest Doubleライブラリとは大きく異なるアプローチをとることで、これを実現しています。
その1 なんでもspy
Test Doubleライブラリの多くには、「test spy」という機能があります。
例えばcrispyだとこんな感じ。
# 対象のメソッドsubject_methodが、渡したfileをcloseするか?
= File.open "hoge"
file
spy_into file
subject_method file
# subject_methodが実際にfileのcloseメソッドを使用していれば、trueが返る。
.received? :close spy(file)
上記のようにspyされたオブジェクトは、自分自身が呼び出したメソッド(と、その引数)をすべて記憶することで、 テストしたいメソッドによって正しく使用されているかチェックすることができるようになります。
crispyは、名前が「spy」で終わっていることが示しているように、この「test spy」をfeatureしたTest Doubleライブラリです。 他のあらゆるTest Doubleとも異なるtest spy機能を持っています。
何が異なるのかと言うと、それは、Rubyのあらゆるオブジェクトに対してspyできる、という点です。
例えばrspec-mocksで先ほどの例を再現すると、
そのものずばりspy
メソッドによって作られる、spy専用のオブジェクトを使わなければなりません1。
# 対象のメソッドsubject_methodが、渡したfileをcloseするか?
= spy 'fileっぽい何か'
file
subject_method file
# subject_methodが実際にfileのcloseメソッドを使用していれば、テストが通る。
.to have_received :close expect(file)
そのため、rspec-mocksを使った上記の例では、実際にfile
のclose
メソッドを使ったり、他のメソッドを呼んだりすることができません。
それに対してcrispyは、普通のRubyのオブジェクトであるfile
にspy機能を「付け足す」ことができるので、file
をそのまま扱うことが出来ます2。
で、何がいいの?
さて、あらゆるオブジェクトに対してspyできると、一体何が嬉しいのでしょう?
それは、テストをより「普通な」順序で書くことができる、という点です。
例えば、先ほどの例で「file
のclose
メソッドを呼んだり、他のメソッドを呼んだり」しつつ、
「対象のメソッドsubject_method
が、渡したfile
をclose
するか?」調べるには、
mockを使って次のように書かないといけません3。
= File.open "hoge"
file
.to receive(:close)
expect(file)
subject_method file# この辺の時点でfileのcloseメソッドが呼ばれていれば、テストが通る。
先ほどの例とは打って変わって、
「対象のメソッドsubject_method
が、渡したfile
をclose
するか?」
と問う処理(RSpec用語で言うところの「expectation」)が、対象のメソッドsubject_method
の前にあります。
RSpecに限らず、自動テストをある程度書いたことがある方なら、これがかなり異例であることはよくご存知でしょう。
crispyはまさにこれをなくすために、あらゆるオブジェクトに対してspyできるようにしたのです。
というのも、例えばここまで何度も出した例が、 とある大きなspecの一部である場合を想像してみてください。
'subject_method' do
describe do
before @file = File.open "hoge"
@result = subject_method file
end
'なにか望ましい値を返す' do
it @result).to eq 'なにか望ましい値'
expect(end
'渡したファイルになにか書き込む' do
it @file.rewind # 書き込んだ値を調べるため、fileの読み込み位置を最初の位置に戻す
@file.read).to eq 'なにか書き込まれた値'
expect(end
'何か他にも状態を変える' do
it .to be_nantoka
expect(nanika)end
# ... その他もろもろ。
# ここまで before ブロックで呼んだ subject_method が起こす影響を
# subject_method の **後に** 書くことでテストしていたのに、
'渡したファイルのcloseメソッドを呼ぶ' do
it # ここだけsubject_methodが起こす影響を **先に** 書かないといけない!
@file).to receive(:close)
expect(@file
subject_method end
end
なんだかもにょりませんか?
通例、私達はテストを通して対象のメソッドの振る舞いを記述する際、
「hogehogeのメソッドを呼ぶとfugafugaな値を返す」と言った具合に、対象のメソッドの戻り値についての期待を書いたり、
「hogehogeのメソッドを呼ぶとbarbarをblahblahな状態に変える」と言った具合に、対象のメソッドが何かに及ぼす影響(副作用)についての期待を書いたりするはずです。
これらは原理的に対象のメソッドを呼んだ後にしか調べることができないため、
対象のメソッドを、それに対する期待より前に書くことが、私達にとってより自然で「普通な」はずです。
そしてこれを常に実現するには、設定した任意のオブジェクトに対してspyする仕組み — つまりcrispyが、なくてはならないのです。
その2 普通のRubyでmessage expectation
crispyの目覚ましい特徴は、もう一つあります。
それは、spyしたオブジェクトが呼び出したメソッドと引数の記録を、received_messages
というメソッドで自由にアクセスできるようにしたことです。
これはすなわち、Rubyの「普通の」Arrayを扱うような感覚で、より複雑な条件のmessage expectation、言い換えればオブジェクトが呼び出したメソッドの記録のテスト、ができるということです。
例を示すために、もう一度fileをcloseする件に帰りましょう。
今度はcloseしたfileを二度と使ってほしくない、という要件から、
file
が呼んだ最後のメソッドがclose
であるかどうか調べたいとします。
そのような場合、crispyでは次のように書くことができます。
# 対象のメソッドsubject_methodが、渡したfileを**最後に**closeするか?
= File.open "hoge"
file
spy_into file
# received_messagesというArrayに、fileが呼び出したすべてのメソッドが記録されているので、
# その最後の記録を見れば、fileが呼んだ最後のメソッドがcloseであるとわかる。
.received_messages.last.method_name == :close
spy(file)# これも同じ意味。
.received_messages[-1].method_name == :close spy(file)
どうでしょう?直感的だと思いませんか?
更に複雑なこともできます。
今度はclose
よりも前にちゃんとread
メソッドを使って、file
を読んでいるどうかも調べてみましょう。
spy(file).received_messages # file が呼んだすべてのメソッドから、
.map(&:method_name) # メソッド名だけを取り出し、
.take_while do|method_name| # close より前のものだけを抜き出し、
= :close
method_name !end
.include?(:read) # read があるか調べる。
実際には大抵の場合、ここまで複雑なことをしなくてもテストしたいことをテストし切るのは簡単でしょう。
しかしながらこの節の例は、spy
とreceived_messages
とmethod_name
を除けば、
すべてRubyのEnumerableやArrayの標準的なAPIを使って書かれています。
crispyはこのように、Rubyを普通に勉強した知識をそのまま活かして、複雑なmessage expectationを書けるようにしてくれます。
そしてそのように勉強したRubyの知識は、テストだけでなく、実装する上でも必ず活かせるはずです。
ピンとこない方はちょうど昨日のRuby Advent Calendar 2014の記事、
tbpgrさんの「条件分岐とループベースのロジックからコレクションパイプラインを利用したロジックへ #ruby」
やRuby公式サイトのドキュメントを読んだりいろいろググったりして、
今から使えるよう勉強してみるのを強くオススメします。
これに対して他のTest Doubleライブラリはいかがでしょうか?
私は少なくとも前述の例をrspec-mocksで実現する方法を知りません4。
他のライブラリはこうした複雑なケースをクリアするためにいろいろ直感的なAPIを考えて実装していますが、
いずれもそれ専用の学習が必要ですし、時々曖昧さを産んでしまうことさえあります(例は申し訳なくも割愛いたします)。
私は日々rspec-mocksを使い、そうしたことに悩みながら、crispyを考え、開発することにしました。
こうした思いに共感される方も、あるいは「ホントかよ〜」なんて疑いの目を持たれる方も、ぜひ一度お試しになって、もろもろフィードバックをください!
https://github.com/igrep/crispy
仕組み
時間がないので割愛しますが、簡単に言うと、指定したオブジェクトの、特異クラスのすべてのメソッドをラップするメソッドをもったmoduleを作り、それをprependしています。
prependが使えないといけないので、あいにくRuby 2.0以降でないと動きません。あしからず。
これから
残念ながら「もうちょっとあれが出来てから〜」なんて渋っているうちに、crispyを現場で使うことがないまま半年近くが過ぎてしまいました。
現在は現場、つまり私の仕事でcrispyをより使いやすくできるよう、
RSpecと組み合わせることができる拡張を開発しています。
もちろんこれがなくてもRSpecと一緒に使うことは出来ますが、より自然言語らしい書き方を好むRSpec-erの気持ちを考えれば、あったほうがいいに決まっています。
職場でこれができる前に採用するかは、現在検討中です。というのも、RSpec(というよりrspec-mocks)と一緒に使えるようにする、というのがなかなか難しそうなので…。
あ、もちろんcrispy自体の拡張はこれからももっともっと行ないます。
英語ですが、Issuesに追加したい機能を簡単にリストアップしています。
日本語でもコメントOKとしますので、「簡潔すぎてわからん!」とか「これも実装しろよ!」なんてコメントも歓迎です。
ただし、日本語でコメントされた場合、私が同意を求めたうえ、英語に訳したものを同じページに載せるかもしれませんが、予めご了承ください。
それではこれからのcrispyの進化に乞うご期待!
次回!
- ソフトウェアテストあどべんとかれんだー2014がa_suenamiさんの「テストとは開発プロセスそのものである」、
- Ruby Advent Calendar 2014がkomiyakさんの「Ruby だけで経験できること」、です!
詳しい人にバカにされないために補足しますと、実際にはrspec-mocksの
spy
はdouble
とほぼ同等のものを返します。 rspec-mocksではdouble
がspyの機能を備えているためです。↩︎もちろん、場合によってはrspec-mocksのspyのような振る舞いのほうがいいこともあるでしょう。 その場合はrspec-mocksと同じように、crispyの
double
メソッドを使いましょう。 rspec-mocksのdouble
と同じように、spyの機能も使えます。
… いや、私が実装し忘れてなければ使えるはずです。 (;^_^)↩︎私の経験上、実際そのようなケースの方が多いように思います。 呼んでいるかどうか調べたいメソッド以外のメソッドも使えた方が、 変更に強いテストが出来るでしょうし、そのメソッドはstubするのではなくそのまま使うほうが、 戻り値の型などをテストする側で意識する必要がなく、楽なので。↩︎
よーく考えたら少なくとも2つ目の例は
receive().ordered
マッチャーを使えば普通に出来そうですね… 失礼しました。↩︎