3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

これは何

とあるModelのコールバックメソッドの中身のテストしようとしたが、FactoryBotの after(:build) の存在に気付かず翻弄されたので、それをスキップする方法をまとめました。

  • after(:build)
    • FactoryBot.build('hoge')が呼ばれた後に実行されるコールバック

きっかけとなったコード

Memberモデルに定義してある、assign_customer_idメソッド内で正しくcustomer_idが設定されるかのテストを書いていました。
実際の動作テストでは成功しますが、RSpecのテストではcustomer_idが設定されずに成功しません。
尚、データ関連の生成には、基本的にはFactoryBotを使用するようにしています。

Modelの例
class Member < ApplicationRecord
  # レコードが生成される直前に呼ばれるActiveRecordのコールバック
  before_create :assign_customer_id

  def assign_customer_id
    # 外部APIと連携してcustomer_idをセットするような処理
  end
end
Rspecの例
  describe '#assign_customer_id' do
    subject do
      member.save!
    end

    let(:member) do
      build(:member)
    end

    it 'customer created' do
      subject
      # 正しくcustomer_idが設定されているかを確かめる処理
    end
  end

原因

今回テストしていたModelと対応するFactoryBot内で、assign_customer_id のメソッドだけがモックに置き換わっているようでした。
そのため、FactoryBotを利用している限りは、絶対に assign_customer_id の中身が実行されない作りになってました。

FactoryBotの例
FactoryBot.define do
  factory :member do
    name { FFaker::Internet.user_name }
    email { FFaker::Internet.email }
    customer_id { '' }

    transient do
      stubs { [:assign_customer_id] }
    end

    after(:build) do |member, evaluator|
      evaluator.stubs.each do |method|
        # ここでモックに置き換えられている
        receive = RSpec::Mocks::Matchers::Receive.new(method, -> {})
        RSpec::Mocks::AllowanceTarget.new(member).to receive
      end
    end
  end
end

記事ではさらっと書いていますが、頭の中にFactoryBotのコールバックに対する意識が抜けていたので、たどり着くまでに時間がかかりました。
※なぜモックになっているかは今回は説明を省きます。

対策

FactoryBotでデータを生成する際に、skip_after_build を指定することで、after(:build)をスキップするようにします。

  • trait
    • FactoryBot内でのデータを定義を簡素にするために用いるオプション
  • transient
    • 作成するデータと直接関係無い新しいattributeを定義する機能
    • 挙動を変更するためのフラグ等に利用するのが一般的
FactoryBotの例
FactoryBot.define do
  factory :member do
    name { FFaker::Internet.user_name }
    email { FFaker::Internet.email }
    customer_id { '' }
    
+    trait :skip_after_build do
+      # 実際に作成するデータと直接関係無い新しいattributeを定義する機能。
+      transient do
+        skip_after_build { true }
+      end
+    end

    transient do
      stubs { [:assign_customer_id] }
    end

    after(:build) do |member, evaluator|
+      # skip_after_buildが定義されていたら処理をスキップする
+      unless defined? evaluator.skip_after_build
        evaluator.stubs.each do |method|
          # ここでモックに置き換えられている
          receive = RSpec::Mocks::Matchers::Receive.new(method, -> {})
          RSpec::Mocks::AllowanceTarget.new(member).to receive
        end
+      end
    end
  end
end
Rspecの例
  describe '#assign_customer_id' do
    subject do
      member.save!
    end

    let(:member) do
-      build(:team)
+      build(:team, :skip_after_build)
    end

    it 'customer created' do
      subject
      # 正しくcustomer_idが設定されているかを確かめる処理
    end
  end

これで無事に書きたいテストが書けました。
割とレアなケースな気がするため、需要があるかはわかりませんが、少しでも参考になれば幸いです。

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?