カスタムの作り方を勉強したのでその備忘録。
確認したバージョン
$ ruby --version
ruby 2.1.3p242 (2014-09-19 revision 47630) [x86_64-darwin12.0]
$ gem list | grep rspec
rspec (3.1.0)
rspec-core (3.1.7)
rspec-expectations (3.1.2)
rspec-mocks (3.1.3)
rspec-support (3.1.2)
新しいマッチャを定義する RSpec::Matchers.define
カスタムマッチャを定義するMatcher DSLと呼ばれるRSpec::Matchers.define
が用意されている。
require "nokogiri"
RSpec::Matchers.define :have_tag do |tag_name|
match do |html_string|
doc = Nokogiri::HTML(html_string)
!doc.css(tag_name.to_s).empty?
end
end
describe "カスタムマッチャーを作ってみるよ" do
subject { '<html><body><div>contents</div></body></html>' }
it { is_expected.to have_tag :div }
end
実行!
$ rspec .
.
Finished in 0.00089 seconds (files took 0.15038 seconds to load)
1 example, 0 failures
できた!
検証失敗時のメッセージを設定する
検証失敗となるテストを追加する。
describe "カスタムマッチャーを作ってみるよ" do
:
it { is_expected.to have_tag :p }
end
実行。
$ rspec .
.F
Failures:
1) カスタムマッチャーを作ってみるよ should have tag :p
Failure/Error: it { is_expected.to have_tag :p }
expected "<html><body><div>contents</div></body></html>" to have tag :p
# ./a_spec.rb:17:in `block (2 levels) in <top (required)>'
expected "<html><body><div>contents</div></body></html>" to have tag :p
とそこそこリーダブルだがHTMLソースが巨大だったら目が眩む。failure_message
で検証失敗時のメッセージが設定できる。
RSpec::Matchers.define :have_tag do |tag_name|
:
failure_message do |html_string|
%Q{HTML( "#{html_string.sub(/(?<=.{20})(.+)$/, '...')}" )にタグ "#{tag_name.to_s}" が含まれる}
end
end
実行結果のメッセージは以下のようになる。
1) カスタムマッチャーを作ってみるよ should have tag :p
Failure/Error: it { is_expected.to have_tag :p }
HTML( "<html><body><div>con..." )にタグ "p" が含まれる
# ./a_spec.rb:17:in `block (2 levels) in <top (required)>'
否定の検証(expect().to
ではなくexpect().not_to
)失敗時のメッセージを設定する
failure_message_when_negated
で設定する。
RSpec::Matchers.define :have_tag do |tag_name|
:
failure_message_when_negated do |html_string|
%Q{HTML( "#{html_string.sub(/(?<=.{20})(.+)$/, '...')}" )にタグ "#{tag_name.to_s}" が含まれない}
end
end
Documentationフォーマットの見栄えをよくする
ワンライナーシンタックス(it
の引数にdescription
を渡さない書き方)の場合、-f d
で実行するDocumentationフォーマットが依然として機械的に構築された文章になっている。
$ rspec . -f d
カスタムマッチャーを作ってみるよ
should have tag :div
should not have tag :p
Finished in 0.00129 seconds (files took 0.15401 seconds to load)
2 examples, 0 failures
description
で任意の文字列を設定できる。
RSpec::Matchers.define :have_tag do |tag_name|
:
description do |html_string|
%Q{HTML( "#{html_string.sub(/(?<=.{20})(.+)$/, '...')}" )にタグ "#{tag_name.to_s}" が含まれる}
end
end
これで見栄えがよくなった。
カスタムマッチャーを作ってみるよ
should HTML( "<html><body><div>con..." )にタグ "div" が含まれる
should not HTML( "<html><body><div>con..." )にタグ "p" が含まれる
先頭のshoud
とshould not
はどうにもならんのかな...(やはり日本語で書くのはイケてない)。
メソッドチェーンで自然言語風DSLなインターフェースにする
例えばこういうの。
it { is_expected.to have_tag(:div).exactly(2).elements }
要素の有無だけではなく、要素数も併せて検証可能としてみる。
chain
でチェーンできるメソッドが定義できる。
RSpec::Matchers.define :have_tag do |tag_name|
:
chain :exactly do |num|
@num = num
end
# 以下は副作用なし、見栄えのためだけに定義
chain :elements do; end
chain :element do; end
end
引数の値を集めておいて、match
ブロック内でそれを利用する。
併せてfailure_message
にもその値を反映させよう。
RSpec::Matchers.define :have_tag do |tag_name|
match do |html_string|
doc = Nokogiri::HTML(html_string)
if @num.nil?
!doc.css(tag_name.to_s).empty?
else
doc.css(tag_name.to_s).size == @num
end
end
failure_message do |html_string|
if @num.nil?
%Q{HTML( "#{html_string.sub(/(?<=.{20})(.+)$/, '...')}" )にタグ "#{tag_name.to_s}" が含まれる}
else
%Q{HTML( "#{html_string.sub(/(?<=.{20})(.+)$/, '...')}" )にタグ "#{tag_name.to_s}" が #{@num} 含まれる}
end
end
:
end
これで以下のように書ける。
describe "カスタムマッチャーを作ってみるよ" do
subject { '<html><body><div>contents</div></body></html>' }
:
it { is_expected.to have_tag(:div).exactly(1).elements }
it { is_expected.to have_tag(:div).exactly(2).elements }
end
実行!
$ rspec .
...F
Failures:
1) カスタムマッチャーを作ってみるよ should HTML( "<html><body><div>con..." )にタグ "div" が含まれる
Failure/Error: it { is_expected.to have_tag(:div).exactly(2).elements }
HTML( "<html><body><div>con..." )にタグ "div" が 2 含まれる
# ./a_spec.rb:43:in `block (2 levels) in <top (required)>'
否定の検証ロジックを通常の検証ロジックと別々にする
match_when_negated
を定義すると、not_to
の場合にmatch
の結果を反転するのではなく、そちらの検証ロジックを利用するようになる。通常の検証ロジックと否定の検証ロジックを別々にしたほうが良い場合に利用する。
RSpec::Matchers.define :have_tag do |tag_name|
match do |html_string|
doc = Nokogiri::HTML(html_string)
if @num.nil?
!doc.css(tag_name.to_s).empty?
else
doc.css(tag_name.to_s).size == @num
end
end
# このサンプルのロジックであれば不要だが、あくまでサンプルなので記述
match_when_negated do |html_string|
doc = Nokogiri::HTML(html_string)
if @num.nil?
doc.css(tag_name.to_s).empty?
else
node_set = doc.css(tag_name.to_s)
node_set.empty? or doc.css(tag_name.to_s).size != @num
end
end
:
end
match_when_negated
ではtrue
を返却するとnot_to
の検証をパスする。
サンプルのカスタムマッチャ+テストケース全容
以下の通り。
require "nokogiri"
RSpec::Matchers.define :have_tag do |tag_name|
match do |html_string|
doc = Nokogiri::HTML(html_string)
if @num.nil?
!doc.css(tag_name.to_s).empty?
else
doc.css(tag_name.to_s).size == @num
end
end
match_when_negated do |html_string|
doc = Nokogiri::HTML(html_string)
if @num.nil?
doc.css(tag_name.to_s).empty?
else
node_set = doc.css(tag_name.to_s)
node_set.empty? or doc.css(tag_name.to_s).size != @num
end
end
failure_message do |html_string|
if @num.nil?
%Q{HTML( "#{html_string.sub(/(?<=.{20})(.+)$/, '...')}" )にタグ "#{tag_name.to_s}" が含まれる}
else
%Q{HTML( "#{html_string.sub(/(?<=.{20})(.+)$/, '...')}" )にタグ "#{tag_name.to_s}" が #{@num} 含まれる}
end
end
failure_message_when_negated do |html_string|
%Q{HTML( "#{html_string.sub(/(?<=.{20})(.+)$/, '...')}" )にタグ "#{tag_name.to_s}" が含まれない}
end
description do |html_string|
%Q{HTML( "#{html_string.sub(/(?<=.{20})(.+)$/, '...')}" )にタグ "#{tag_name.to_s}" が含まれる}
end
chain :exactly do |num|
@num = num
end
chain :elements do; end
chain :element do; end
end
describe "カスタムマッチャーを作ってみるよ" do
subject { '<html><body><div>contents</div></body></html>' }
it { is_expected.to have_tag :div }
it { is_expected.not_to have_tag :p }
it { is_expected.to have_tag(:div).exactly(1).elements }
it { is_expected.not_to have_tag(:div).exactly(2).elements }
it { is_expected.not_to have_tag(:p).exactly(0).elements }
it { is_expected.not_to have_tag(:p).exactly(1).elements }
end
雑記
RSpec::Matchers.define
のブロック内DSL
RSpec::Matchers::DSL::Macrosで定義されている。
Matcherプロトコル
Matcher DSLではなく特定のメソッドを持つクラスを定義することでカスタムマッチャを作成できる。
matches?
failure_message
を最低限実装する必要があり、全てのI/FはRSpec::Matchers::MatcherProtocolに定義されている。
例えばビルトインのeq
マッチャは実態はRSpec::Matchers::BuiltIn::Eq
クラスでMatcherプロトコルに準拠している。
failure_message_for_should
どこいった?
3.0.0.beta2でdeprecatedになっている。deprecatedになっているメソッドはRSpec::Matchers::DSL::Macros::Deprecatedに集められて、利用するとwarning吐くようになっている。
Matcher DSLとMatcherプロトコル、どう使い分けるべき?
まだ分からん。Matcherプロトコルのほうが柔軟に書けそうだが、どういったケースでそのメリットを享受できるのかがイメージがついていない。誰か教えて...。