RSpecでカスタムマッチャを作る

カスタム:tea:の作り方を勉強したのでその備忘録。

# 確認したバージョン

```
$ 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`が用意されている。

```rb
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
```

できた！

## 検証失敗時のメッセージを設定する

検証失敗となるテストを追加する。

```rb 
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`で検証失敗時のメッセージが設定できる。

```rb
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`で設定する。

```rb
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`はどうにもならんのかな...(やはり日本語で書くのはイケてない:see_no_evil:)。

## メソッドチェーンで自然言語風DSLなインターフェースにする

例えばこういうの。

```rb
it { is_expected.to have_tag(:div).exactly(2).elements }
```

要素の有無だけではなく、要素数も併せて検証可能としてみる。

`chain`でチェーンできるメソッドが定義できる。

```rb
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`にもその値を反映させよう。

```rb
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
```

これで以下のように書ける。

```rb 
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`の結果を反転するのではなく、そちらの検証ロジックを利用するようになる。通常の検証ロジックと否定の検証ロジックを別々にしたほうが良い場合に利用する。

```rb
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`の検証をパスする。

## サンプルのカスタムマッチャ+テストケース全容

以下の通り。

```rb
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](http://www.rubydoc.info/gems/rspec-expectations/RSpec/Matchers/DSL/Macros)で定義されている。

## Matcherプロトコル

Matcher DSLではなく特定のメソッドを持つクラスを定義することでカスタムマッチャを作成できる。

- `matches?`
- `failure_message`

を最低限実装する必要があり、全てのI/Fは[RSpec::Matchers::MatcherProtocol](http://www.rubydoc.info/gems/rspec-expectations/RSpec/Matchers/MatcherProtocol)に定義されている。

例えばビルトインの`eq`マッチャは実態は[`RSpec::Matchers::BuiltIn::Eq`クラス](https://github.com/rspec/rspec-expectations/blob/master/lib/rspec/matchers/built_in/eq.rb)でMatcherプロトコルに準拠している。

## `failure_message_for_should`どこいった？

[3.0.0.beta2](https://github.com/rspec/rspec-expectations/blob/master/Changelog.md#300beta2--2014-02-17)でdeprecatedになっている。deprecatedになっているメソッドは[RSpec::Matchers::DSL::Macros::Deprecated](http://www.rubydoc.info/gems/rspec-expectations/RSpec/Matchers/DSL/Macros/Deprecated)に集められて、利用するとwarning吐くようになっている。

## Matcher DSLとMatcherプロトコル、どう使い分けるべき？

まだ分からん。Matcherプロトコルのほうが柔軟に書けそうだが、どういったケースでそのメリットを享受できるのかがイメージがついていない。誰か教えて...。