【rails】rspecでcontrollerのconcern単体テストをやる方法

ことの始まり

巨大なcontrollerを分割した際、共通で使うメソッドはconcernにまとめておこうと考え。
controllerの単体テストでわざわざconcernの内容も一緒にやるのは馬鹿らしいので、concernの単体テストをやりたくなったが、ちょっと困ったので色々調べた。
せっかくなので、忘れないようにまとめておこう。

ベースとなるconcern

今回作ったのはこんな感じのconcern。

module HogeConcern
  extend ActiveSupport::Concern

  def fuga(order)
    # ハッシュを返し、nameキーに'maccho_'をつける
  end

  def piyo
    piyo_params = params.permit(~~) # permitの中は省略
    # 頭に'maccho_'をつけたstrを返す
  end
end

fugaメソッドは引数にorderを持っている。
一方で、piyoメソッドはpost時に渡されたparamsをそのまま使っている。

引数がある場合

まずは引数がある場合から。
とりあえずテストのベースとなる部分を作っておく。

require 'rails_helper'

describe HogeConcern do
  context '.fuga' do
    let(:order) do
      FactoryBot.buid(:order, {name: hoge_name})
    end
    
    subject { fuga(order) }
    
    context 'hoge_name' do
      let(:hoge_name) { 'yasagori' }
      it { is_expected.to eq({ name: 'maccho_yasagori' }) }
    end
  end
end

当然、このままだと通らない。
まずは、structを使ってテスト用のクラスを作る。
クラスを作る際に、HogeConcernを忘れずにincludeしておく。
そして、作成したクラスをnewする。

  context '.fuga' do
    let(:test_class) { Struct.new(:concern) { include HogeConcern } }
    let(:concern) { test_class.new }
    let(:order) do
      FactoryBot.buid(:order, {name: hoge_name})
    end
    
    subject { concern.fuga(order) }
    
    context 'hoge_name' do
      let(:hoge_name) { 'yasagori' }
      it { is_expected.to eq({ name: 'maccho_yasagori' }) }
    end
  end

concern自体はcontrollerにincludeして使われているので、単純にメソッドを呼び出しても使えない、ということ。
なのでstructを使って擬似的にクラスを作ってあげてからincludeすることでメソッドを呼び出せるようになる。
呼び出すときはsubjectのような書きっぷりが必要(メソッドの呼び出しだから)

post parameterを使う場合

問題はこっち。
controllerがないのにどうやってpost parameterを作ればいいんだ…
結論を先に言うと、擬似的にcontrollerを作ってあげる必要があった。

describe HogeConcern, type: :controller do # typeをcontrollerにする
  context '.piyo' do
    controller(ApplicationController) do
      include HogeConcern

      def fake_action
        @piyo = piyo
      end
    end

    before { routes.draw {
      post 'fake_action' => 'anonymous#fake_action'
    } }

    subject { controller.instance_variable_get("@piyo") }
    
    context 'piyo_str' do
      before { post :fake_action, params: { name: 'yasagori' } }
      it { is_expected.to eq('maccho_yasagori') }
    end
  end
end

まずcontrollerを設定する。
concernのテストをしたいので、includeを忘れずに。
その中で、呼び出すメソッドを定義しておく。
定義したメソッドはroutes.drawでrouteの定義付をしてあげる。
subjectはメソッドの中で定義したインスタンス変数を呼び出すためcontroller.instance_variable_getを使った。
これで、controllerテストの中でconcernのテストを行う必要がなくなる。

まとめ

結果的にconcernの単体テストでpost parameterを呼び出そうとすると、controllerの設定が必要になってしまった。
が、それぞれのcontrollerのなかでテストを行う必要がなくなったので、最終的なコードは短くまとめられ、すっきりさせられた。