Factory GirlとRspecのコールバックをスキップします


103

テスト中、特定の状況でのみ実行したいafter createコールバックを使用してモデルをテストしています。ファクトリからのコールバックをスキップ/実行するにはどうすればよいですか?

class User < ActiveRecord::Base
  after_create :run_something
  ...
end

工場:

FactoryGirl.define do
  factory :user do
    first_name "Luiz"
    last_name "Branco"
    ...
    # skip callback

    factory :with_run_something do
      # run callback
  end
end

回答:


111

それが最良の解決策であるかどうかはわかりませんが、私はこれを使用して正常にこれを達成しました:

FactoryGirl.define do
  factory :user do
    first_name "Luiz"
    last_name "Branco"
    #...

    after(:build) { |user| user.class.skip_callback(:create, :after, :run_something) }

    factory :user_with_run_something do
      after(:create) { |user| user.send(:run_something) }
    end
  end
end

コールバックなしで実行:

FactoryGirl.create(:user)

コールバックで実行:

FactoryGirl.create(:user_with_run_something)

3
あなたはスキップしたい場合は:on => :create、検証を、使用after(:build) { |user| user.class.skip_callback(:validate, :create, :after, :run_something) }
ジェームズ・シュヴァリエ

7
スキップするコールバックロジックを逆にした方がいいのではないでしょうか。つまり、デフォルトでは、オブジェクトを作成するときにコールバックがトリガーされ、例外的な場合には別のパラメーターを使用する必要があります。したがって、FactoryGirl.create(:user)はコールバックをトリガーするユーザーを作成し、FactoryGirl.create(:user_without_callbacks)はコールバックなしのユーザーを作成する必要があります。これは単なる "設計"の変更であることはわかっていますが、既存のコードを壊すことを避け、より一貫性を持たせることができると思います。
Gnagno 2013年

3
@Minimalのソリューションが示すように、Class.skip_callback呼び出しは他のテスト間で永続的であるため、他のテストがコールバックの発生を予期している場合、スキップするコールバックロジックを反転させようとすると失敗します。
mpdaugherty、2014

私は結局、after(:build)ブロックでのモカでのスタブに関する@uberllamaの回答を使用することになりました。これにより、出荷時のデフォルトでコールバックが実行され、使用するたびにコールバックをリセットする必要がありません。
mpdaugherty 2014

これが他の方法で機能することについて何か考えはありますか?stackoverflow.com/questions/35950470/...
クリス・ハフ

89

コールバックを実行したくない場合は、次のようにします。

User.skip_callback(:create, :after, :run_something)
Factory.create(:user)

skip_callbackは実行後に他の仕様全体で永続化されることに注意してください。したがって、次のようなものを検討してください。

before do
  User.skip_callback(:create, :after, :run_something)
end

after do
  User.set_callback(:create, :after, :run_something)
end

12
コールバックのスキップはクラスレベルでハングするため、後続のテストでコールバックをスキップし続けることを明示的に述べているため、この回答の方が好きです。
siannopollo 14

私もこれが好きです。私の工場に永続的に異なる動作をさせたくありません。特定の一連のテストではスキップします。
theUtherSide 2017年

39

これらのソリューションはどれも優れていません。クラスからではなく、インスタンスから削除する必要がある機能を削除することにより、クラスを改ざんします。

factory :user do
  before(:create){|user| user.define_singleton_method(:send_welcome_email){}}

コールバックを抑制する代わりに、コールバックの機能を抑制しています。ある意味では、このアプローチはより明確であるため、このアプローチの方が好きです。


1
私はこの答えが本当に好きです。そして、意図がすぐに明確になるようにエイリアスされたこのような何かがFactoryGirl自体の一部である必要があるかどうか疑問に思います。
ジュゼッペ

私もこの回答が好きなので、他のすべてに反対票を投じますが、コールバックが同種の場合around_*(たとえばuser.define_singleton_method(:around_callback_method){|&b| b.call })、定義されたメソッドにブロックを渡す必要があるようです。
Quv

1
より良い解決策だけでなく、何らかの理由で他の方法ではうまくいきませんでした。私がそれを実装したとき、それはコールバックメソッドが存在しないと言いましたが、それを省略したとき、それは私に不必要なリクエストをスタブするように頼みました。それは私を解決策に導きますが、なぜそれがそうなのかを誰かが知っていますか?
Babbz77 2018

27

@luizbrancoの回答を改善して、他のユーザーを作成するときにafter_saveコールバックをより再利用できるようにしたいと思います。

FactoryGirl.define do
  factory :user do
    first_name "Luiz"
    last_name "Branco"
    #...

    after(:build) { |user| 
      user.class.skip_callback(:create, 
                               :after, 
                               :run_something1,
                               :run_something2) 
    }

    trait :with_after_save_callback do
      after(:build) { |user| 
        user.class.set_callback(:create, 
                                :after, 
                                :run_something1,
                                :run_something2) 
      }
    end
  end
end

after_saveコールバックなしで実行:

FactoryGirl.create(:user)

after_saveコールバックで実行:

FactoryGirl.create(:user, :with_after_save_callback)

テストでは、デフォルトでコールバックなしでユーザーを作成することをお勧めします。これは、使用するメソッドが、テスト例では通常必要ない余分なものを実行するためです。

---------- UPDATE ------------テストスイートに不整合の問題があったため、skip_callbackの使用を中止しました。

代替ソリューション1(スタブとアンスタブの使用):

after(:build) { |user| 
  user.class.any_instance.stub(:run_something1)
  user.class.any_instance.stub(:run_something2)
}

trait :with_after_save_callback do
  after(:build) { |user| 
    user.class.any_instance.unstub(:run_something1)
    user.class.any_instance.unstub(:run_something2)
  }
end

代替ソリューション2(私の推奨アプローチ):

after(:build) { |user| 
  class << user
    def run_something1; true; end
    def run_something2; true; end
  end
}

trait :with_after_save_callback do
  after(:build) { |user| 
    class << user
      def run_something1; super; end
      def run_something2; super; end
    end
  }
end

これが他の方法で機能することについて何か考えはありますか?stackoverflow.com/questions/35950470/...
クリス・ハフ

RuboCopは、Alternative Solution 2の「Style / SingleLineMethods:シングルラインメソッド定義を回避する」と不満を言っているので、フォーマットを変更する必要がありますが、それ以外は完璧です!
コベリン

14

Rails 5- skip_callbackFactoryBotファクトリからスキップすると、引数エラーが発生します。

ArgumentError: After commit callback :whatever_callback has not been defined

Rails 5では、skip_callbackが認識されないコールバックを処理する方法に変更がありました。

ActiveSupport :: Callbacks#skip_callbackは、認識されないコールバックが削除された場合にArgumentErrorを発生させるようになりました

skip_callbackがファクトリから呼び出されたとき、ARモデルの実際のコールバックはまだ定義されていません。

あなたがすべてを試し、私のようにあなたの髪を引っ張ったなら、ここにあなたの解決策があります(FactoryBotの問題を検索してそれを手に入れました)そのraise: false部分に注意してください):

after(:build) { YourSweetModel.skip_callback(:commit, :after, :whatever_callback, raise: false) }

あなたが好む他のどんな戦略でもそれを自由に使ってください。


1
これはまさに私に起こったことです。コールバックを一度削除してから再試行すると、これが発生するため、ファクトリーに対してこれが複数回トリガーされる可能性が非常に高いことに注意してください。
slhck

6

このソリューションは私にとってはうまくいき、あなたのファクトリー定義に追加のブロックを追加する必要はありません:

user = FactoryGirl.build(:user)
user.send(:create_without_callbacks) # Skip callback

user = FactoryGirl.create(:user)     # Execute callbacks

5

単純なスタブがRspec 3で私に最も適していました

allow(User).to receive_messages(:run_something => nil)

4
あなたはそれをセットアップする必要があると思いインスタンスUser:run_somethingはクラスメソッドではありません。
PJSCopeland 2015年

5
FactoryGirl.define do
  factory :order, class: Spree::Order do

    trait :without_callbacks do
      after(:build) do |order|
        order.class.skip_callback :save, :before, :update_status!
      end

      after(:create) do |order|
        order.class.set_callback :save, :before, :update_status!
      end
    end
  end
end

重要な注意事項 として、両方を指定する必要があります。beforeのみを使用して複数のスペックを実行する場合、コールバックを複数回無効にしようとします。初回は成功しますが、2回目はコールバックが定義されなくなります。エラーが発生します


これにより、最近のプロジェクトのスイートでいくつかの難読化されたエラーが発生しました。@ Sairamの回答に似たものがありましたが、テスト間のクラスでコールバックが設定されていませんでした。おっと。
kfrz

4

私の工場からskip_callbackを呼び出すと、問題が発生することがわかりました。

私の場合、作成の前後にいくつかのs3関連のコールバックを含むドキュメントクラスがあり、フルスタックのテストが必要な場合にのみ実行する必要があります。それ以外の場合は、これらのs3コールバックをスキップします。

ファクトリーでskip_callbacksを試したところ、ファクトリーを使用せずに直接ドキュメントオブジェクトを作成した場合でも、コールバックスキップが持続しました。そのため、代わりに、ビルド後の呼び出しでモカスタブを使用し、すべてが完全に機能しています。

factory :document do
  upload_file_name "file.txt"
  upload_content_type "text/plain"
  upload_file_size 1.kilobyte
  after(:build) do |document|
    document.stubs(:name_of_before_create_method).returns(true)
    document.stubs(:name_of_after_create_method).returns(true)
  end
end

ここでは、すべてのソリューションを、工場内のロジックを持つため、これはで動作する唯一のものであるbefore_validationフック(やろうとしskip_callbackFactoryGirlののいずれかとbeforeafterのオプションbuildcreate仕事しませんでした)
マイク・T

3

これは現在のrspec構文(この投稿の時点)で動作し、よりクリーンです。

before do
   User.any_instance.stub :run_something
end

これはRspec 3で非推奨になりました。通常のスタブを使用すると、以下の私の回答を参照できます。
samg 2014年

3

before_validationコールバックをスキップする方法に関するJames Chevalierの回答は私を助けませんでした。

モデル内:

before_validation :run_something, on: :create

工場で:

after(:build) { |obj| obj.class.skip_callback(:validation, :before, :run_something) }

2
これは避けた方がいいと思います。(ファクトリーガールによって生成されたものだけでなく)クラスのすべてのインスタンスのコールバックをスキップします。これにより、仕様の実行に関するいくつかの問題が発生し(初期ファクトリのビルド後に無効化が発生した場合)、デバッグが困難になる可能性があります。:これは仕様で必要な動作である場合には/それが明示的に行われるべきでサポート Model.skip_callback(...)
ケビン・シルベスタ

2

私の場合、コールバックでredisキャッシュに何かをロードしています。しかし、私は自分のテスト環境で実行されているredisインスタンスを持っていませんでした。

after_create :load_to_cache

def load_to_cache
  Redis.load_to_cache
end

私の状況では、上記と同様load_to_cacheに、spec_helperで自分のメソッドをスタブしました。

Redis.stub(:load_to_cache)

また、これをテストする特定の状況では、対応するRspecテストケースのbeforeブロックでそれらをアンスタブする必要があります。

私はあなたがあなたの中でもっと複雑なことが起こっているafter_createかもしれないか、これがとてもエレガントではないかもしれないことを知っています。この記事の「コールバックのキャンセル」セクションによると、after_createファクトリでフックを定義することにより、モデルで定義されたコールバックをキャンセルすることができます(factory_girlのドキュメントを参照)。(コールバックが実行される順序がわからないため、このオプションを選択しませんでした)。false

最後に(申し訳ありませんが、記事を見つけることができません)Rubyでは、ダーティーメタプログラミングを使用して、コールバックフックのフックを解除できます(リセットする必要があります)。これは最も好ましいオプションではないでしょう。

実際には解決策ではなく、もう1つありますが、実際にオブジェクトを作成する代わりに、仕様でFactory.buildを使用できるかどうかを確認してください。(できれば最も簡単でしょう)。


2

上記の回答https://stackoverflow.com/a/35562805/2001785については、コードをファクトリーに追加する必要はありません。仕様自体でメソッドをオーバーロードする方が簡単だと思いました。たとえば、代わりに(引用された投稿のファクトリコードと組み合わせて)

let(:user) { FactoryGirl.create(:user) }

私は使用するのが好きです(引用された工場コードなし)

let(:user) do
  FactoryGirl.build(:user).tap do |u|
      u.define_singleton_method(:send_welcome_email){}
      u.save!
    end
  end
end

この方法では、テストの動作を理解するために、ファクトリファイルとテストファイルの両方を調べる必要はありません。


1

次の解決策は、コールバックがクラスレベルで実行/設定されるため、よりクリーンな方法であることがわかりました。

# create(:user) - will skip the callback.
# create(:user, skip_create_callback: false) - will set the callback
FactoryBot.define do
  factory :user do
    first_name "Luiz"
    last_name "Branco"

    transient do
      skip_create_callback true
    end

    after(:build) do |user, evaluator|
      if evaluator.skip_create_callback
        user.class.skip_callback(:create, :after, :run_something)
      else
        user.class.set_callback(:create, :after, :run_something)
      end
    end
  end
end

0

これを一般的な方法で処理するために作成したスニペットを次に示します。
のようなRails関連のコールバックを含め、構成されたすべてのコールバックをスキップしますが、 before_save_collection_association自動生成されたautosave_associated_records_for_コールバックなど、ActiveRecordを正常に動作させるために必要なものはスキップしません。

# In some factories/generic_traits.rb file or something like that
FactoryBot.define do
  trait :skip_all_callbacks do
    transient do
      force_callbacks { [] }
    end

    after(:build) do |instance, evaluator|
      klass = instance.class
      # I think with these callback types should be enough, but for a full
      # list, check `ActiveRecord::Callbacks::CALLBACKS`
      %i[commit create destroy save touch update].each do |type|
        callbacks = klass.send("_#{type}_callbacks")
        next if callbacks.empty?

        callbacks.each do |cb|
          # Autogenerated ActiveRecord after_create/after_update callbacks like
          # `autosave_associated_records_for_xxxx` won't be skipped, also
          # before_destroy callbacks with a number like 70351699301300 (maybe
          # an Object ID?, no idea)
          next if cb.filter.to_s =~ /(autosave_associated|\d+)/

          cb_name = "#{klass}.#{cb.kind}_#{type}(:#{cb.filter})"
          if evaluator.force_callbacks.include?(cb.filter)
            next Rails.logger.debug "Forcing #{cb_name} callback"
          end

          Rails.logger.debug "Skipping #{cb_name} callback"
          instance.define_singleton_method(cb.filter) {}
        end
      end
    end
  end
end

じゃあ後で:

create(:user, :skip_all_callbacks)

言うまでもなく、YMMVなので、実際にスキップしているものをテストログで確認してください。多分あなたは本当に必要なコールバックを追加する宝石を持っていて、それはあなたのテストを無惨に失敗させるでしょう、あるいはあなたが100のコールバック脂肪モデルからあなたは特定のテストのためにカップルが必要です。そのような場合は、一時的な:force_callbacks

create(:user, :skip_all_callbacks, force_callbacks: [:some_important_callback])

ボーナス

検証をスキップする必要がある場合もあり(すべてテストを高速化するため)、次のことを試してください。

  trait :skip_validate do
    to_create { |instance| instance.save(validate: false) }
  end

-1
FactoryGirl.define do
 factory :user do
   first_name "Luiz"
   last_name "Branco"
   #...

after(:build) { |user| user.class.skip_callback(:create, :after, :run_something) }

trait :user_with_run_something do
  after(:create) { |user| user.class.set_callback(:create, :after, :run_something) }
  end
 end
end

コールバックを実行したい場合は、それらのインスタンスの特性をコールバックに設定するだけです。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.