Rails で認証や認可、例外処理など Controller を横断して呼び出されるロジックは、ApplicationController などに実装されることが多い。 ApplicationController のメソッドに対してテストを追加しようとするが、それ自体は route および action を持っていないためテストの仕方に迷う。テストの方法について検索すると
などが出てきたが、RSpec が公式として推奨している方法はなさそうだったので、自分なりの方針を考えてみた。
ApplicationController のテストを 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 や RSpec の公式(コアチーム)は、Controller Spec よりも Request Spec を用いて Controller の action に関わるテストを作成するようにと推奨している。
Controller Spec を用いても ApplicationController のテストを行うことは可能であり、かつテスト対象の Controller のインスタンスを直接参照可能であったり、Anonymous Controllerのような便利なヘルパーもあったりする。
しかしながら、RSpec 公式としての方針になるべく則っておくことが今後のメンテナンス性の担保につながると考えている。
ApplicationController のロジックを Concerns や PORO に切り出し、責務を分けることで単体テストにする手法がある。 もちろん Controller の責務を切り出していくことは前提としつつ
ため、Controller 層でも結合テストはしておきたい。
ApplicationController のメソッドを Request Spec でテストする場合に、そもそもテスト対象の route および対応する action が存在しないという問題がある。
上記については
を考慮すると、Request Spec として自然な形でテストを作成できると考えている。
例えば、ApplicationController の認証ロジックをテストした場合は
spec/requests/authentication_spec.rbやspec/requests/auth_spec.rbをファイル名として、以下のような Spec を作成する。
require 'rails_helper'
RSpec.describe 'Authentication', type: :request do
# ...
end
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
ApplicationController のテストを Request Spec として作成した場合の課題としては、リクエスト対象の route に対応する action を持つ Controller のインスタンスを直接参照できないことである。(特定の route にリクエストを送信することが前提になるため)
この場合、例えば、logger.errorなど ActionController のインスタンスメソッドを stub して、ログ出力を検証したい場合に困る。
やむなくallow_any_instance_ofを使った(rubocop に怒られる)ので、いいやり方があれば誰か教えてほしい。
GitLab のテストコードでは、allow_next_instance_ofというヘルパーを作成して、呼び出し予定の Controller のメソッドを stub していたりする。