複数あるエンドポイントについてデフォルトで実行したいフィルタを、before_action
として親コントローラに登録したいことがありました。
当初は親コントローラでメソッド定義とフィルタの登録をして、テストは各エンドポイントのテストで行うやり方を考えていました。
しかしその方針では、今後エンドポイントを追加したい時にも毎回同じテストを書き続けることが必要になってしまい、将来的に追加漏れが発生する恐れがあることに気づきました。
仮に追加が漏れて問題が無かったとしても「なぜこのテストがあるファイルとないファイルがあるのだ」という余分な迷いを生んでしまいそうに思われました。
そこで次のような方法で実装することにしました。
- フィルタを
concern
に切り出す - フィルタのテストは一時的なコントローラを作ってテストする
- 親コントローラがその
concern
をincludeしていることをテストする
# concern module RequireActiveUser extend ActiveSupport::Concern included do before_action :user_must_be_active end private def user_must_be_active if user.inactive? # ... end end end
# 親コントローラ class ParentController < ActionController::API include RequireActiveUser # ... end
# concernのテスト require 'rails_helper' RSpec.describe RequireActiveUser, type: :controller do describe '#user_must_be_active' do controller do include RequireActiveUser def index head :ok end end context '有効なユーザーのとき' do # ... end context '有効でないユーザーのとき' do # ... end end end
# 親コントローラがconcernをincludeしていることのテスト RSpec.describe ParentController, type: :controller do # ... describe 'RequireActiveUser' do it 'RequireActiveUserモジュールをincludeしている' do expect(controller.class.ancestors).to include(RequireActiveUser) end end end
まとめ・感想
上記のような設計にすることで、今後この親コントローラの下に新たなエンドポイントを追加する時に都度このフィルタに関するテストを追加することが不要になりました。
コントローラごとに挙動が変わるわけではないコードについて全てのコントローラでテストをすると、線形にコードが増えていって好ましくなさそうと学びました。
またこの後に別のサービスで同様の実装をしたい機会があった時には、子コントローラの中にいくつか「フィルタを実行したくないエンドポイント」があったことから、フィルタを実行したい子コントローラでconcern
をincludeし、各子コントローラのテストでconcern
をincludeしていることを検証するという設計にしました。
なおその時には「今後このアプリケーションが拡張される可能性が少ない(将来的な追加漏れを考慮する必要が少ない)」という点も考慮されていたので、今回のやり方を一つの引き出しとして、その時々のアプリケーションの状態を考えながらうまく使えたら良いなと思いました。