読者です 読者をやめる 読者になる 読者になる

おもしろwebサービス開発日記

Ruby や Rails を中心に、web技術について書いています

Chapter 17 Extending RSpec その2

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と判断する。