The RSpec Book を読んで、知らなかった部分のメモが主。
describe / context
describe は example group をつくる。example group は 1 つのクラス (RSpec::ExampleGroup::…) として表される。ネストした describe は、外側の example group のサブクラスになる。
describe "root" do
it "print ancestors" do
p self.class.ancestors # => [RSpec::Core::ExampleGroup::Nested_1]
end
describe "nested" do
it "print ancestors" do
p self.class.ancestors # => [RSpec::Core::ExampleGroup::Nested_1::Nested_1, RSpec::Core::ExampleGroup::Nested_1]
end
end
end
サブクラスなので、include や def で定義したインスタンスメソッド、インスタンス変数、などが共有される。before / after / around フックも共有され、before は外側から順に、after は内側から順に適用される
context は describe の alias なので、まったく同じように動作する。
pending の使い方
- ブロックなしの it
単純に pending であることが示される。まだなにも example を書いていないが、忘れないために書く。
- it ブロック中の pending (ブロックなし)
pending が書かれた時点で example の評価を止める。適切な example を書いていない状態。
- it ブロック中の pending (ブロックあり)
pending に渡すブロックが失敗すれば pending、成功すれば failure になる。すぐに成功させる必要のない example に。コードを修正し、成功すれば pending ブロックを削除する。
before / after / around
after は before や example 中でエラーが起こったり failure しても実行される。around の yield 以降は同じ状況では実行されない (begin … ensure 文が必要)。可読性に劣るので通常は before / after を使う (around を定義するクラスメソッドをつくるなどはありうる)。
# around の書き方
around do |example|
foo
example.run
bar
end
around do
foo
yield
bar
end
すべての example に適用したいフックは config で設定する。
RSpec.configure do |config|
config.before do
# some code
end
config.after do
# some code
end
end
ヘルパーメソッド
describe ブロック中 で def したメソッドは it ブロック中で呼び出しできる (describe で生成されるクラスのインスタンスメソッド呼び出し)。
describe Foo do
def new_foo
Foo.new
end
it "should create" do
new_foo.should be_a(Foo)
end
end
複数の example group で使用するなら module に抽出して、describe 中で include する。
module FooHelper
def new_foo
Foo.new
end
end
describe Foo do
include FooHelper
it "should create" do
new_foo.should be_a(Foo)
end
end
さらに多くの example group で使うなら、下記のコードでモジュールを include する。
RSpec.configure do |config|
config.include(FooHelper)
end
shared_examples_for
複数のコンテキストで同じ example group を使う場合 shared_examples_for でまとめ、it_behaves_like で使える。
shared_examples_for ブロック内では before / after なども使える。
マクロのほうが柔軟だけど、意図が明確になる。
shared_examples_for "collection" do
it "should be respond to each" do
@collection.should be_respond_to(:each)
end
end
describe Array do
before(:each) do
@collection = []
end
it_behaves_like "collection"
end
describe Hash do
before(:each) do
@collection = {}
end
it_behaves_like "collection"
end
Matcher
Matcher の仕組みと作り方
should
、should_not
のあとに続く (文法的には引数になる) 部分を
matcher
と呼ぶ。matcher
は下記のメソッドに応答するオブジェクト。
- matches(actual) - should / should_not のレシーバを引数にして呼ばれ、期待したものなら true / 異なれば false を返す。
- failure_message - should が失敗したときに呼ばれるメソッド。表示するメッセージを文字列で返す。
- negative_failure_message - should_not が失敗したときに呼ばれるメソッド。表示するメッセージを文字列で返す。
たとえば、ある数と一致することを期待する matcher は以下のようになる
class IntegerMatcher
def initialize(number)
@expect = number.to_i
end
def matches?(actual)
@actual = actual.to_i # メッセージで使うため、インスタンス変数に保存しておく
@actual == @expect
end
def failure_message_for_should
"#{@expect} expected, but was #{@actual}"
end
def failure_message_for_should_not
"Except #{@expect} expected, but was #{@actual}"
end
end
使い方は下記の通り。
describe "number" do
it "1 equals 1" do
1.should IntegerMatcher.new(1)
end
it "1 does not equal 2" do
1.should_not IntegerMatcher.new(2)
end
it "1 equals 2" do
1.should IntegerMatcher.new(2) # 2 expected, but was 1
end
it "1 does not equals 1" do
1.should_not IntegerMatcher.new(1) # Except 1 expected, but was 1
end
end
これをより DSL 的にする。
describe "number" do
def equal_integer(expected)
IntegerMatcher.new(expected)
end
it "1 equals 1" do
1.should equal_integer(1)
end
it "1 does not equal 2" do
1.should_not equal_integer(2)
end
it "1 equals 2" do
1.should equal_integer(2)
end
it "1 does not equals 1" do
1.should_not equal_integer(1)
end
end
この、クラスの定義とオブジェクト生成メソッドの定義を簡単に行うための仕組みがある。
RSpec::Matchers.define :equal_integer do |expected|
@expected = expected.to_i
match do |actual|
@actual = actual.to_i
actual == expected
end
failure_message_for_should do |actual|
"#{@expected} expected, but was #{@actual}"
end
failure_message_for_should_not do |actual|
"Except #{@expected} expected, but was #{@actual}"
end
description do
# Some description for this matcher
end
end
これですべての Example Group で使えるので、通常はこの方法で定義する。
組み込み Matcher
組み込みの Matcher は rspec-expectations gem の lib/rspec/matchers 下に定義されている。
- be.rb
- be_true
- be_false
- be_nil
- be_* : *? を呼び出し truthy であることを期待する。be_a_*、be_an_* でもよい。?メソッドの最後の s は省略可能。
- be_a : kind_of? が true になることを期待する
- be ==, be <, be <=, be >=, be >, be ===
- be_close.rb
- be_close(expected, delta) : Float に使う。expected から誤差 delta 内に収まる。
- be_instance_of.rb
- be_instance_of(klass) : (be_* で実現可能では?)
- be_kind_of.rb
- be_kind_of(klass) : (be_* で実現可能では?)
- be_within.rb
- be_within(delta).of(expected) : be_close と同様
- block_aliases
- expect{}.to, expect{}.to_not, expect{}.not_to : expect{}.should / expect{}.should_not のエイリアス
- change.rb
- change{}, change{}.by(delta), change{}.from(old).to(new) : ブロックを actual.call の前後に評価して違いを期待する
- cover.rb
- cover(values) : actual.cover?(value) が true となることを期待する
- eq.rb
- eq(value) : == と同じ
- eql.rb
- eql(value) : eql? が true になることを期待する
- equal.rb
- equal(value) : equal? が true になることを期待する
- exist.rb
- exist(value) : exist? または exists? が true になることを期待する。両方ある場合、両方評価され、値が異なる場合、例外が発生。
- has.rb
- have_* : has_*? を呼び出した返値が true であることを期待する
- have.rb
- have(n).*, have_at_least(n).*, have_at_most(n).* : * の返値の size か length が n と同じ、n 以上、n 以下であることを期待する。actual 自体の size / length を検証したい場合は * に items / characters を使う。
- include.rb
- include(value) : ややこしいので後述
- match.rb
- match(pattern)
- match_array.rb
- =~ : array のすべての要素が同じことを期待する。要素数が異なれば失敗、順序は自由。
- operator_matcher.rb
-
,
=, =~, >, >=, <, <=
-
- raise_error.rb
- raise_error, raise_error(exception_class), raise_error(exception_class, message_string_or_regexp) : actual.call により例外が発生することを期待する
- respond_to.rb
- respond_to(method_names), respond_to(method_names).with(n).arguments : arguments は 省略しても argument でも可
- satisfy.rb
-
satisfy{ a …} : ブロックの評価値が true となることを期待する - throw_symbol(expect) : actual.call により throw expect されることを期待する
-
include
両方 Hash なら expected のすべての key-value が actual にある
expected.all?{|k| e.all?{|k, v| actual[k] == v}}
expected.none?{|k| e.any?{|k, v| actual[k] == v}}
actual が Hash で expected が Hash でなければ、expected がすべて存在するキーである
expected.all?{|k| actual.has_key?(k)}
expected.none?{|k| actual.has_key?(k)}
それ以外は、expected のすべての値を actual.include? に渡して true であることを期待する
expected.all?{|k| actual.include?(k)}
expected.none?{|k| actual.include?(k)}
Rails 関連
request.remote_addr
request.remote_ip を使い、リモートホストの IP
アドレスによって挙動を変えている際に、controller の spec で、
request.remote_addr = "127.0.0.1"
などとしてもうまくいかない。これは
ActionDispatch::TestRequest#ip でキャッシュしているため。下記のパッチを
spec/support/test_request.rb に置く。
# remote_addr= を使うため、ipをキャッシュしないようにする
class ActionDispatch::TestRequest
def ip
# @ip ||= super
@ip = super
end
end