rhanda | 元銀行員Web系エンジニアの日記

実務未経験からWeb系受託開発企業に転職したひよっこエンジニアが覚えたことや日々の感情を残すブログ

親コントローラに入れる処理をテストしやすくするための設計

複数あるエンドポイントについてデフォルトで実行したいフィルタを、before_actionとして親コントローラに登録したいことがありました。

当初は親コントローラでメソッド定義とフィルタの登録をして、テストは各エンドポイントのテストで行うやり方を考えていました。
しかしその方針では、今後エンドポイントを追加したい時にも毎回同じテストを書き続けることが必要になってしまい、将来的に追加漏れが発生する恐れがあることに気づきました。
仮に追加が漏れて問題が無かったとしても「なぜこのテストがあるファイルとないファイルがあるのだ」という余分な迷いを生んでしまいそうに思われました。


そこで次のような方法で実装することにしました。

# 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していることを検証するという設計にしました。

なおその時には「今後このアプリケーションが拡張される可能性が少ない(将来的な追加漏れを考慮する必要が少ない)」という点も考慮されていたので、今回のやり方を一つの引き出しとして、その時々のアプリケーションの状態を考えながらうまく使えたら良いなと思いました。