kakudooo docs

rspec-rails で ApplicationController をテストする

Rails で認証や認可、例外処理など Controller を横断して呼び出されるロジックは、ApplicationController などに実装されることが多い。 ApplicationController のメソッドに対してテストを追加しようとするが、それ自体は route および action を持っていないためテストの仕方に迷う。テストの方法について検索すると

などが出てきたが、RSpec が公式として推奨している方法はなさそうだったので、自分なりの方針を考えてみた。

方針

ApplicationController のテストを Request Spec に書く。

理由としては

Controller Spec よりも Request Spec に

The official recommendation of the Rails team and the RSpec core team is to write request specs instead. Request specs allow you to focus on a single controller action, but unlike controller tests involve the router, the middleware stack, and both rack requests and responses. This adds realism to the test that you are writing, and helps avoid many of the issues that are common in controller specs. In Rails 5, request specs are significantly faster than either request or controller specs were in rails 4

Rails: Support for Rails5

にあるように、Rails や RSpec の公式(コアチーム)は、Controller Spec よりも Request Spec を用いて Controller の action に関わるテストを作成するようにと推奨している。

Controller Spec を用いても ApplicationController のテストを行うことは可能であり、かつテスト対象の Controller のインスタンスを直接参照可能であったり、Anonymous Controllerのような便利なヘルパーもあったりする。

しかしながら、RSpec 公式としての方針になるべく則っておくことが今後のメンテナンス性の担保につながると考えている。

結合テストも書きたい

ApplicationController のロジックを Concerns や PORO に切り出し、責務を分けることで単体テストにする手法がある。 もちろん Controller の責務を切り出していくことは前提としつつ

ため、Controller 層でも結合テストはしておきたい。

ApplicationController のテストを Request Spec に書く

ApplicationController のメソッドを Request Spec でテストする場合に、そもそもテスト対象の route および対応する action が存在しないという問題がある。

上記については

  1. Request Spec をテスト観点を名称にして作成する
  2. テスト対象となる route を適当にピックアップして使用する

を考慮すると、Request Spec として自然な形でテストを作成できると考えている。

1. Request Spec をテスト観点を名称にして作成する

例えば、ApplicationController の認証ロジックをテストした場合は

spec/requests/authentication_spec.rbspec/requests/auth_spec.rbをファイル名として、以下のような Spec を作成する。

require 'rails_helper'

RSpec.describe 'Authentication', type: :request do
  # ...
end

2. テスト対象となる route を適当にピックアップして使用する

1.のようにテスト観点を名称とすることで、特定の route をピックアップして使っても特に違和感がない。Anonymous Controllerのようにテストのためだけの route を作成したくなるところだが、例えば認証ロジックが ApplicationController に実装されている時点で、少なくとも一つは認証が必要な route が作成されているはずである。

GitLab でも ApplicationController のテストを Request Spec で行っている。

describe 'session expiration' do
  context 'when user is authenticated' do
    it 'sets the redis_expiry option from session_expire_delay setting' do
      sign_in(user)

      get root_path # root_pathをテスト対象としてピックアップ

      expect(request.env['rack.session.options'][:redis_expiry]).to eq(
        Settings.gitlab['session_expire_delay'] * 60
      )
    end
  end

  context 'when user is unauthenticated' do
    it 'sets the redis_expiry option from unauthenticated_session_expire_delay' do
      get root_path # root_pathをテスト対象としてピックアップ

      expect(request.env['rack.session.options'][:redis_expiry]).to eq(
        Settings.gitlab['unauthenticated_session_expire_delay']
      )
    end
  end
end

参考: https://github.com/gitlabhq/gitlabhq/blob/8bb96e278b3c8ba00d384378d14343996ee36a2e/spec/requests/application_controller_spec.rb#L24-L46

Request Spec の課題

ApplicationController のテストを Request Spec として作成した場合の課題としては、リクエスト対象の route に対応する action を持つ Controller のインスタンスを直接参照できないことである。(特定の route にリクエストを送信することが前提になるため)

この場合、例えば、logger.errorなど ActionController のインスタンスメソッドを stub して、ログ出力を検証したい場合に困る。 やむなくallow_any_instance_ofを使った(rubocop に怒られる)ので、いいやり方があれば誰か教えてほしい。

GitLab のテストコードでは、allow_next_instance_ofというヘルパーを作成して、呼び出し予定の Controller のメソッドを stub していたりする。

参考: https://github.com/gitlabhq/gitlabhq/blob/8bb96e278b3c8ba00d384378d14343996ee36a2e/gems/gitlab-rspec/lib/gitlab/rspec/next_instance_of.rb#L12C7-L12C29

参考