Skip callbacks on Factory Girl and Rspec
我正在使用创建后回调回调来测试模型,该回调仅在测试时仅在某些情况下运行。 如何跳过/运行工厂中的回调?
1 2 3 4 | class User < ActiveRecord::Base after_create :run_something ... end |
厂:
1 2 3 4 5 6 7 8 9 10 11 | FactoryGirl.define do factory :user do first_name"Luiz" last_name"Branco" ... # skip callback factory :with_run_something do # run callback end end |
我不确定这是否是最好的解决方案,但是我已经成功实现了以下目标:
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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 |
在没有回调的情况下运行:
1 | FactoryGirl.create(:user) |
使用回调运行:
1 | FactoryGirl.create(:user_with_run_something) |
当您不想运行回调时,请执行以下操作:
1 2 | User.skip_callback(:create, :after, :run_something) Factory.create(:user) |
请注意,skip_callback在运行后将在其他规范中保持不变,因此请考虑以下内容:
1 2 3 4 5 6 7 | before do User.skip_callback(:create, :after, :run_something) end after do User.set_callback(:create, :after, :run_something) end |
这些解决方案都不是好的。它们通过删除应该从实例而不是从类中删除的功能来破坏类。
1 2 | factory :user do before(:create){|user| user.define_singleton_method(:send_welcome_email){}} |
我没有取消回调,而是取消了回调的功能。在某种程度上,我更喜欢这种方法,因为它更加明确。
我想对@luizbranco的答案进行改进,以在创建其他用户时使after_save回调更可重用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | 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回调的情况下运行:
1 | FactoryGirl.create(:user) |
使用after_save回调运行:
1 | FactoryGirl.create(:user, :with_after_save_callback) |
在我的测试中,我更喜欢默认情况下创建不带回调的用户,因为使用的方法运行的是我在测试示例中通常不想要的东西。
----------更新------------
我停止使用skip_callback,因为测试套件中存在一些不一致的问题。
替代解决方案1(使用存根和存根):
1 2 3 4 5 6 7 8 9 10 11 | 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(我的首选方法):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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 |
此解决方案对我有效,您无需在Factory定义中添加其他块:
1 2 3 4 | user = FactoryGirl.build(:user) user.send(:create_without_callbacks) # Skip callback user = FactoryGirl.create(:user) # Execute callbacks |
从FactoryBot工厂跳过时,Rails 5-
1 | ArgumentError: After commit callback :whatever_callback has not been defined |
Rails 5中有一个更改,其中skip_callback如何处理无法识别的回调:
ActiveSupport::Callbacks#skip_callback now raises an ArgumentError if an unrecognized callback is remove
从工厂调用
如果您已尝试了所有步骤并像我一样拉了头发,这是您的解决方案(可从搜索FactoryBot问题中获得)(注意
1 | after(:build) { YourSweetModel.skip_callback(:commit, :after, :whatever_callback, raise: false) } |
随意将其与您喜欢的任何其他策略一起使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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 |
重要说明,您应同时指定两者。
如果仅在之前使用并运行多个规范,它将尝试多次禁用回调。它将第一次成功,但是第二次将不再定义回调。所以会出错
一个简单的存根在Rspec 3中最适合我
1 | allow(User).to receive_messages(:run_something => nil) |
从我的工厂调用skip_callback对我来说是有问题的。
就我而言,我在创建之前和之后都有一个文档类,其中包含一些与s3相关的回调,我只想在需要测试整个堆栈时才运行。否则,我想跳过那些s3回调。
当我在工厂中尝试过skip_callbacks时,即使我不使用工厂直接创建文档对象时,它仍然保留了回调跳过。因此,相反,我在after build调用中使用了mocha存根,并且一切运行正常:
1 2 3 4 5 6 7 8 9 | 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 |
James Chevalier关于如何跳过before_validation回调的答案对我没有帮助,因此如果您遇到与我相同的问题,这是可行的解决方案:
在模型中:
1 | before_validation :run_something, on: :create |
在工厂:
1 | after(:build) { |obj| obj.class.skip_callback(:validation, :before, :run_something) } |
这将与当前的rspec语法一起使用(截至本文),并且更加简洁:
1 2 3 | before do User.any_instance.stub :run_something end |
就我而言,我有回调将某些内容加载到我的Redis缓存中。但是后来我没有/想要在我的测试环境中运行redis实例。
1 2 3 4 5 | after_create :load_to_cache def load_to_cache Redis.load_to_cache end |
对于我的情况,与上面类似,我只是在spec_helper中添加了
与:
1 | Redis.stub(:load_to_cache) |
另外,在某些情况下,我想对此进行测试,我只需要在相应的Rspec测试用例的before模块中将它们取消存根即可。
我知道您在
最后,(抱歉,我无法找到该文章)Ruby允许您使用一些肮脏的元编程来解开回调钩子(您必须将其重置)。我猜这将是最不受欢迎的选择。
好吧,还有一件事,不是真正的解决方案,而是看看您是否可以按照自己的规格使用Factory.build,而不是实际创建对象。 (如果可以的话,将是最简单的)。
关于上面发布的答案,https://stackoverflow.com/a/35562805/2001785,您不需要将代码添加到工厂。我发现更容易在规范本身中重载方法。例如,而不是(与引用的文章中的工厂代码一起)
1 | let(:user) { FactoryGirl.create(:user) } |
我喜欢使用(没有引用的工厂代码)
1 2 3 4 5 6 7 | let(:user) do FactoryGirl.build(:user).tap do |u| u.define_singleton_method(:send_welcome_email){} u.save! end end end |
这样,您无需查看工厂文件和测试文件即可了解测试的行为。
我发现以下解决方案是一种更干净的方法,因为回调是在类级别运行/设置的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | # 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 |
这是我创建的用于以通用方式处理此代码的代码段。
它将跳过配置的每个回调,包括与Rails相关的回调,例如
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | # 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 |
然后再:
1 | create(:user, :skip_all_callbacks) |
不用说,YMMV,因此请查看测试日志中您真正跳过的内容。也许您有一个gem添加了您真正需要的回调,它将使您的测试失败,或者从您的100个回调胖模型中,您只需要几个就可以进行特定的测试。对于这些情况,请尝试使用瞬态
1 | create(:user, :skip_all_callbacks, force_callbacks: [:some_important_callback]) |
奖金
有时您还需要跳过验证(都是为了使测试更快),然后尝试:
1 2 3 | trait :skip_validate do to_create { |instance| instance.save(validate: false) } end |
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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 |
您可以在需要运行时为那些实例设置一个带有特征的回调。