ValidationのテストをRSpecカスタムマッチャにしたやつをGemにしたよ

# 以前

http://qiita.com/nisshiee/items/98087befe9ce3e6e6b01

こんな記事書いた。
RSpecカスタムマッチャ作った話。

- RailsのValidationをテストするのに、実際にattributeに入れる値に紐付けて結果を確認したい
    - この値だったらvalid、この値だったらinvalidといった具合
- で、それを簡単にできるカスタムマッチャ作ったよ、というのが前回
    - `is { is_expected.to be_invalid_on(:name).with(nil) }` って感じで書ける


# その後

- 結構社内で使われるようになった
- 一部独自拡張されながら、コピーが量産されてしまった

嬉しいけど、このままは良くない。


# というわけで

Gemにしたよ！

- [validation_examples_matcher](https://github.com/nisshiee/validation_examples_matcher)

使い方はREADME読んでくれー
Gemfileに書くだけで使えるはず


# めんどかったこと

**テスト**。

RSpecカスタムマッチャでfailするケースを普通に書いちゃったら、テストfailするやんｗ、と。

というわけで、2種類のテストに分けた。


## examples spec

こっちは、ライブラリユーザと同じように、このカスタムマッチャを用いてModelのValidationのテストを書くパターン。これはさっき言ったように、failするパターンは書けない。なので、 `to` と `not_to` を使うだけでOKとする。

```rb:spec/examples/validations_spec.rb
context 'when target attribute has valid value' do
  it { is_expected.to be_valid_on(:attr).with('valid value') }
  it { is_expected.not_to be_invalid_on(:attr).with('valid value') }
end
```

## unit test

しかし、 `to` と `not_to` を使い分けるだけだと、結局テストがfailしないので、failしたときだけ通る `failure_message` ブロックをテストできない。なので、ここは単体テストでカバーする。

という方針自体は良いのだが、問題は、RSpecのカスタムマッチャはDSLを使って定義するってとこ。クラスやメソッドの定義じゃない。こんな感じ。

```rb
RSpec::Matchers.define :be_valid_on do |attr_name|
  match do |model|
    .....
  end

  chain :with do |value|
    .....
  end

  failure_message do |model|
    .....
  end
end
```

なので、 `match` `chain` `failure_message` に渡しているブロック自体を取り出さないと、単体テストできない。

しょーがないので、 `RSpec::Matchers` のところをモックで置き換えた。

```rb:lib/validation_examples_matcher.rb
module ValidationExamplesMatcher
  def self.register(matchers = RSpec::Matchers)
    matchers.define do
      ......
    end
  end

  register
end
```

カスタムマッチャ定義は、 `RSpec::Matchers` を引数で受け取るクラスメソッドにくるんでおいてから呼び出す形に変更。

で、以下の様なモックを作成。

```rb:spec/spec_helper.rb
class MatchersMock < BasicObject
  attr_reader :defined_matchers, :match_blocks, :chain_blocks, :failure_message_blocks
  attr_reader :value, :context

  def define(name, &block)
    @defined_matchers ||= []
    @defined_matchers << name
    @current = name
    instance_exec(:attr, &block)
  end

  def match(&block)
    @match_blocks ||= {}
    @match_blocks[@current] = block
  end

  def chain(name, &block)
    @chain_blocks ||= {}
    @chain_blocks[@current] ||= {}
    @chain_blocks[@current][name] = block
  end

  def failure_message(&block)
    @failure_message_blocks ||= {}
    @failure_message_blocks[@current] = block
  end
end
```

モックの中身でやってることは主に3つ。

- `define` がコールされたら、引数のブロックを `instance_exec` で実行
    - すると、 `match` などの他のメソッドがこのモックオブジェクトのスコープでコールされる
- `match` などの `define` 意外のメソッドでは、渡されたブロックを保管しておく
- テストで検証対象になる値にreaderを作っておく

最後に、このモックを引数にして、マッチャ定義のところで作っておいたメソッドを呼び出す。

```rb:spec/unit_tests/be_invalid_on_matcher_spec.rb
RSpec.describe 'be_invalid_on matcher' do
  let(:matchers) { MatchersMock.new }
  before { ValidationExamplesMatcher.register(matchers) }
end
```

ここまでやると、モックオブジェクトに各ブロックが保管されている状態になるので、ブロックを取り出して実行し、結果を検証することができる。ブロックの実行は、再度モックオブジェクトを使って `instance_exec` する。

```rb:spec/unit_tests/be_invalid_on_matcher_spec.rb
describe 'with on_context block' do
  it 'sets @context' do
    matchers.instance_exec(:a_context, &on_context_chain_block)
    expect(matchers.context).to eq :a_context
  end
end
```


# さいごに

とこんな感じでGemを出すにあたり十分なテストは用意したつもりですが、もしもっと良いやり方があったら教えてくださいｍｍ