Cahpter 17 Extending RSpec その1 - おもしろWEBサービス開発日記の続き。
17.2 Custom Example Groups
describeメソッドは Spec::Example::ExampleGroup のサブクラスを作る。itメソッドはそのサブクラスのメソッド。
Spec::Example::ExampleGroupは下記のように定義されている。
module Spec module Example class ExampleGroup extend Spec::Example::ExampleGroupMethods include Spec::Example::ExampleMethods end end end
上記を見て分かるように、 Spec::Example::ExampleGroup は実際には ExampleGroupMethodes と ExampleMethods モジュールのラッパー。RSpecでは標準でこれを使うけど、他のを使うようにすることも出来る。例えばTest::Unit::TestCaseとRSpecを両方使うようなことも出来る。
custom example groupをcustomするための三つの方法がある。
- Spec::Example::ExampleGroupのサブクラスを作る?(we can subclass Spec::Example::ExampleGroupって書いてあった)
- 自分でclass or moduleを書いて Spec::Example::ExampleGroup にincludeやexcludeする
- TestUnit や Minitest などといった別のライブラリを加える。
Resistering a custom default example group class
ExampleGroupFactoryを使って、example groupに指定したクラスを使うようにできる。
Spec::Example::ExampleGroupFactory.default(CustomExampleGroup)
これにより、
- describe メソッドは CustomExampleGroup の subclass を作るようになる。
- CustomExampleGroup は定数(Spec::ExampleGroup)に定義されて、デフォルトの基本クラスとして元の Spec::Example::ExampleGroup か CustomExampleGroup が参照されることが保証される。
もし何かのライブラリが独自の ExampleGroup を使っていたとして、ユーザがそれを改良したければ Spec::ExampleGroup を再オープンすればよい(独自のクラスの名前を気にせず改良できる)。
Named example group classes
例えばrspec-railsでは、特定の model、controller、view、helper、routing毎に別のexample groupが必要になる。例えば controller は get, post, should render_template などが必要だけど、ModelはDBをexample毎に隔離するような方法が必要だったりする。場面毎に異なるexample group classを使い分けるためには、ExampleGroupFactoryにkeyつきでclassを登録するとよい。
class ControllerExampleGroup Spec::Example::ExampleGroupFactory.register(:controller, self) end
上記のように登録したexample groupを使うには二通りの方法がある。一つ目は下記のようにdescribeの引数に:type => keyを指定する方法。
describe WidgetsController, :type => :controller do # ... end
二つ目の方法は、rspec-railsを使っている場合に使える。ExampleGroupFactoryが :type を見つけられないと、宣言されたgroupのpathをみてkeyを判断する。例えばgroupがspec/controllers配下のファイルで定義されていたら、ExampleGroupFactoryは"controllers"を取り出して:controllers をkeyとして使う。
typeがなくて、pathに結びついたsubclassがない場合、ExampleGroupFactoryはデフォルトのexample group classのサブクラスを作る。
17.3 Custom Matchers
例えば render_template などは rspec-rails が定義している custom matchers。
仮に、joe.should report_to(beatrice) と書けるようなcustom matcherを作るとする。そのためには下記のように書く。
Spec::Matchers.define :report_to do |boss| match do |employee| employee.reports_to?(boss) end end
上記のように定義したmatcherがfailureな時はデフォルトでは下記のようなmessageを返す。
expected <Employee: Joe> to report to <Employee: Beatrice>
employee オブジェクトの部分は to_s メソッドの実装次第で変わる。report toはdefineで登録するときのシンボルから来てる。message をデフォルトから変えたいときには下記のようにする。
Spec::Matchers.define :report_to do |boss| match do |employee| employee.reports_to?(boss) end failure_message_for_should do |employee| "expected the team run by #{boss} to include #{employee}" end failure_message_for_should_not do |employee| "expected the team run by #{boss} to exclude #{employee}" end description do "expected a member of the team run by #{boss}" end end
descriptionメソッドはsuccess時に表示される文章(たぶんitの引数が無いときに表示される)。
上記のような手法では、blockをとるような custom matcherを作るのは難しい(Ruby的に)。がんばれば出来るかもしれないけど、こういう時はMatcher Protocolを使った方が楽。
build-inなmatcherではchangeがblockをとれる。
account = Account.new lambda do account.deposit(Money.new(50, :USD)) end.should change{ account.balance }.by(Money.new(50, :USD))
Matcher Protocol
RSpecのmactherは共通のメソッドを持ってる。必須なのは下記の二つ。
- matches?
- shouldやshould_notメソッドで使われる。
- failure_message_for_should
- failureな時に使われる。
下記のように定義する。
class ReportTo def initialize(manager) @manager = manager end def matches?(employee) @employee = employee employee.reports_to?(@manager) end def failure_message_for_should "expected #{@employee} to report to #{@manager}" end end def report_to(manager) ReportTo.new(manager) end
この手法はcustom matcherを作るよりも明らかに冗長だし、failure messageを表示するためのメソッドも必須(custom matcherはデフォルトのが用意されている)。でもより詳細な設定が出来る。
他にもshouldやshould_notのために定義できるメソッドがいくつかある(オプション)
- faiure_message_for_should_not
- shoud_not用のfailure message
- description
- custom matcherと一緒。success時のmessage
- does_not_match?
- あまり使われない。shouldかshould_notで呼ばれたかどうかを知りたい時に有効なメソッド(shouldとshould_notで処理を分けたいときに定義するといいってことかな)。should_not は does_not_match?を呼ぶ(もし定義されていれば)。実行されたときの戻り値が true なら success で false なら failure と判断する。does_not_match?が定義されていない場合はshould_notはmatch?を呼び出して(matches?の誤植?)falseならsuccessでtrueならfailureと判断する。