RSpecのRouting specsでformatの指定を省略できるようにする

3 min read読了の目安(約3200字

確認したバージョン

  • ruby: 2.7.1
  • rails: 6.0.3
  • rspec-rails: 4.0.1

ルーティングのテストの書き方

基本

ルーティングのテストはリクエストのメソッドとパスの組み合わせがどのコントローラー、アクションに対応付けられているか、どんなパラメータを受けとっているかなどを検証することができます。

  it 'routes to #index' do
    expect(get: '/users').to route_to('users#index')
  end

  it 'routes to #show' do
    expect(get: '/users/1').to route_to('users#show', id: '1')
  end

formatを指定した場合

例えばJSON APIに対するルーティングを定義したい場合、 format: 'json' と書くことでformatを指定することができます。

Rails.application.routes.draw do
  namespace :api, format: 'json' do
    resources :users
  end
end

これに対して以下のようなテストを書くとどうなるでしょうか。

  it 'routes to #show' do
    expect(get: '/api/users/1').to route_to('api/users#show', id: '1')
  end

このテストは以下のように format が一致しないので失敗してしまいます。

Failure/Error: expect(get: '/api/users/1').to route_to('api/users#show', id: '1')

  The recognized options <{"format"=>"json", "controller"=>"api/users", "action"=>"show", "id"=>"1"}> did not match <{"id"=>"1", "controller"=>"api/users", "action"=>"show"}>, difference:.
  --- expected
  +++ actual
  @@ -1 +1 @@
  -{"id"=>"1", "controller"=>"api/users", "action"=>"show"}
  +{"format"=>"json", "controller"=>"api/users", "action"=>"show", "id"=>"1"}

このテストを成功させるには以下のように format を明示すればOKです。

  it 'routes to #show' do
    expect(get: '/api/users/1').to route_to('api/users#show', id: '1', format: 'json')
  end

とはいえAPI以下のルーティングのテストを追加する場合にいちいち format を追記するのも面倒なので、なんとかできないものかと考えていました。

route_toをカスタマイズする

rspec-railsのコードを見ると route_to は以下のように定義されていました。

  def route_to(*expected)
    RouteToMatcher.new(self, *expected)
  end

ここでMatcherに渡している expectedformat を追加してしまえばいいと考え、以下のように route_to を定義しました。

# spec/custom_matchers.rb
module CustomMatchers
  def route_to(*expected)
    hash_index = expected.index { |e| e.is_a?(Hash) }
    if hash_index.nil?
      expected.push({ format: 'json' })
    elsif !expected[hash_index].key?(:format)
      expected[hash_index][:format] = 'json'
    end
    RSpec::Rails::Matchers::RoutingMatchers::RouteToMatcher.new(self, *expected)
  end
end

これにより expected にハッシュがなければ配列に { format: 'json' } を追加、 expected にハッシュが含まれ :format のキーが無ければハッシュに format: 'json' を追加します。

ここで定義した route_to を使うために rails_helper.rb に以下のように設定を追加します。

# rails_helper.rb
...
# Add additional requires below this line. Rails is not loaded until this point!
require 'custom_matchers'
...
RSpec.configure do |config|
  ...
  config.include CustomMatchers
  ...
end

これにより以下のように route_toformat を追記した状態と同じとみなすことができるようになりました。
format が指定されていればそのままになります)

カスタマイズした route_to もとの route_to だと
route_to('api/users#index') route_to('api/users#index', format: 'json')
route_to('api/users#show', id: '1') route_to('api/users#show', id: '1', format: 'json')
route_to('api/users#foo, format: 'bar') route_to('api/users#foo, format: 'bar'

自分はAPIモードで開発をする場合、基本的に format: 'json' を指定するので route_to 自体を置き換えましたが、影響が大きいと思う場合は適当に他の名前をつけて使い分けることもできます。