カスタムマッチャーをカジュアルに書こう

## カスタムマッチャー書いてますか？
以前[こちらの記事](https://qiita.com/masaakiaoyagi/items/36ce6c88fa2b6deb95e0)で、`shared_examples`ではなくカスタムマッチャーで書いた方が読みやすいRSpecになると思うと書きました。
では、カスタムマッチャーはどう書いたらいいでしょうか？

---
### カスタムマッチャーの書き方
上の記事で例題として上げていた、以下のように書けるようにする`be_api_response_of`マッチャーを考えてみましょう。
```ruby
it { expect(response).to be_api_response_of(401, "Unauthorized") }

context "when logged in" do
  include_context "logged in"

  context "when page is 1" do
    let(:page) { 1 }
    it { expect(response).to be_api_response_of(200, "Succeed").with_data(...) }
  end

  context "when page is 'a'" do
    let(:page) { "a" }
    it { expect(response).to be_api_response_of(400, "Bad Request").with_error("page must be an integer") }
  end
end
```

---
#### DSLで書く
よく例として書かれているグローバルにDSLで定義する場合は、例えば以下のような感じでしょうか。
```ruby
# spec/suport/matchers/be_api_response_of.rb のようなところに置いて rails_helper.rb で require されるようにする
# 以下のコメントアウトを外せばいいと思います
# Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f }
RSpec::Matchers.define :be_api_response_of do |expected_status, expected_message|
  chain :with_data do |expected_data|
    @expected_data = expected_data
  end

  chain :with_error do |expected_error|
    @expected_errors ||= [] << expected_error
  end

  match do |actual_response|
    if actual_response.status != 200
      @failure_message = "HTTP status: expected=200, actual=#{actual_response.status}"
      return false
    end
    unless actual_response.content_type.starts_with?("application/json")
      @failure_message = "Content Type: expected=application/json, actual=#{actual_response.content_type}"
      return false
    end
    actual_body = JSON.parse(actual_response.body) rescue {}
    if actual_body["status"] != expected_status
      @failure_message = "status: expected=#{expected_status}, actual=#{actual_body["status"]}"
      return false
    end
    if actual_body["message"] != expected_message
      @failure_message = "message: expected=#{expected_message}, actual=#{actual_body["message"]}"
      return false
    end
    if @expected_data && actual_body["data"] != @expected_data
      @failure_message = "data: expected=#{@expected_data}, actual=#{actual_body["data"]}"
      return false
    end
    if @expected_errors && actual_body["errors"].sort != @expected_errors.sort
      @failure_message = "errors: expected=#{@expected_errors}, actual=#{actual_body["errors"]}"
      return false
    end
    true
  end

  failure_message do
    @failure_message
  end
end
```

---
#### scoped DSLで書く
Example Group内で`matcher`DSLで定義すると、そのブロック内にスコープを絞ることができます。
```ruby
RSpec.describe "users", type: :request do
  matcher :be_api_response_of do |expected_status, expected_message|
    # DSL版と内容は同じ
  end
  # test case...
end
```

---
#### matcher classで書く
以下のように matcher class で定義する事もできます。
```ruby
# rails_helper.rbでincludeする
RSpec.configure do |config|
  config.include MyApp::Rspec::Matchers
end

# spec/suport/matchers/be_api_response_of.rb のようなところに置いて rails_helper.rb で require されるようにする
module MyApp
  module Rspec
    module Matchers
      def be_api_response_of(expected_status, expected_message)
        BeApiResponseOf.new(expected_status, expected_message)
      end

      class BeApiResponseOf < RSpec::Matchers::BuiltIn::BaseMatcher
        def initialize(expected_status, expected_message)
          super()
          @expected_status = expected_status
          @expected_message = expected_message
        end

        def with_data(expected_data)
          @expected_data = expected_data
          self
        end

        def with_error(expected_error)
          @expected_errors ||= [] << expected_error
          self
        end

        def matches?(actual_response)
          # DSL版のmatchとほどんど同じ
          # expected_statusとexpected_messageが、@expected_statusと@expected_messageに変わるだけ
        end

        def failure_message
          @failure_message
        end
      end
    end
  end
end
```

---
### それぞれの特徴
* DSL
  * グローバルに定義される
  * コンテキストに依存したマッチャーが書けてしまう
  * コードの再利用がしづらい
  * 引数の数の違いに気づきづらい
* scoped DSL
  * グローバルに影響しないので、雑に書きやすい
  * 他はDSL版と同じ
* matcher class
  * includeをどこでするかによって、グローバルに使えるようにもできるし、スコープを絞ることもできる
  * 少し書くのが面倒

---
### まとめ
理想は matcher class で定義するのがいいと思うのですが、いきなりそうしなくても他への影響がない scoped DSL でカジュアルに書いてしまって、そこから必要であればグローバルの DSL、matcher class にしていくのも手かなと考えています。
特に`shared_examples`を使ってしまっていて読みづらいRSpecをリファクタリングする際に、scoped DSLにしていくところから始めるとやりやすいように思います。

---
### もっとカジュアルにカスタムマッチャーを書いていくのはどうでしょう？