我正在测试一个带有创建后回调的模型,我只想在测试时在某些情况下运行该回调。如何从工厂跳过/运行回调?
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
我不确定这是否是最好的解决方案,但我已经成功地实现了这一点:
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)
当您不想运行回调时,请执行以下操作:
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
这些解决方案都不好。他们通过删除应该从实例而不是类中删除的功能来破坏类。
factory :user do
before(:create){|user| user.define_singleton_method(:send_welcome_email){}}
end
我没有抑制回调,而是抑制了回调的功能。在某种程度上,我更喜欢这种方法,因为它更明确。
around_*
的亲属(例如 user.define_singleton_method(:around_callback_method){|&b| b.call }
)。
我想改进@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
Rails 5 - 从 FactoryBot 工厂跳过时,skip_callback 引发参数错误。
ArgumentError: After commit callback :whatever_callback has not been defined
有一个 change in Rails 5 说明 skip_callback 如何处理无法识别的回调:
如果移除了无法识别的回调,ActiveSupport::Callbacks#skip_callback 现在会引发 ArgumentError
从工厂调用 skip_callback
时,AR 模型中的真正回调尚未定义。
如果您已经尝试了所有方法并像我一样拔掉了头发,那么这是您的解决方案 (got it from searching FactoryBot issues)(注意 raise: false
部分):
after(:build) { YourSweetModel.skip_callback(:commit, :after, :whatever_callback, raise: false) }
随意将它与您喜欢的任何其他策略一起使用。
此解决方案对我有用,您不必在工厂定义中添加额外的块:
user = FactoryGirl.build(:user)
user.send(:create_without_callbacks) # Skip callback
user = FactoryGirl.create(:user) # Execute callbacks
一个简单的存根在 Rspec 3 中最适合我
allow_any_instance_of(User).to receive_messages(:run_something => nil)
User
的 instances 设置它; :run_something
不是类方法。
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 并运行多个规范,它会尝试多次禁用回调。第一次它会成功,但第二次,回调将不再被定义。所以会报错
从我的工厂调用 skip_callback 对我来说是个问题。
就我而言,我有一个文档类,在创建前后有一些与 s3 相关的回调,我只想在需要测试完整堆栈时运行。否则,我想跳过那些 s3 回调。
当我在我的工厂中尝试 skip_callbacks 时,即使我直接创建了一个文档对象,它仍然保持回调跳过,而不使用工厂。因此,我在构建后调用中使用了 mocha 存根,一切都运行良好:
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
挂钩工作的解决方案(尝试使用 FactoryGirl 的 before
或 after
选项中的 {5 } 和 create
不起作用)
这将适用于当前的 rspec 语法(截至本文)并且更简洁:
before do
User.any_instance.stub :run_something
end
James Chevalier 关于如何跳过 before_validation 回调的回答对我没有帮助,所以如果你和我一样,这里是可行的解决方案:
在模型中:
before_validation :run_something, on: :create
在工厂:
after(:build) { |obj| obj.class.skip_callback(:validation, :before, :run_something) }
Model.skip_callback(...)
这是一个较老的问题,有一些很好的答案,但由于某些原因,它们都不适合我
不喜欢在运行时修改某些类的行为的想法
不想在我的整个课程中都使用 attr_accessor,因为将仅用于测试的逻辑放在模型中似乎很奇怪
不想在各种规范的块之前/之后调用 rspec 到存根/非存根行为
使用 FactoryBot
您可以在您的工厂中使用 transient
来设置一个开关来修改您的类的行为。结果,工厂/规格看起来像
#factory
FactoryBot.define do
factory :user do
transient do
skip_after_callbacks { true }
end
after(:build) do |user, evaluator|
if evaluator.skip_after_callbacks
class << user
def callback_method1; true; end
def callback_method2; true; end
def callback_method3; true; end
end
end
end
end
end
# without running callbacks
user = create(:user)
# with running callbacks for certain specs
user = create(:user, skip_after_callbacks: false)
这对我有用,因为我们的应用程序具有某些方法,这些方法是由于运行到外部服务的各种 after_create/after_commit
回调而触发的,因此默认情况下,我通常不需要在规范中运行这些方法。这样做可以节省我们的测试套件使用 VCR 进行的各种调用。 YMMV
就我而言,我有回调将某些内容加载到我的 redis 缓存中。但是后来我没有/想要为我的测试环境运行 redis 实例。
after_create :load_to_cache
def load_to_cache
Redis.load_to_cache
end
对于我的情况,与上面类似,我只是在我的 spec_helper 中存根了我的 load_to_cache
方法,其中:
Redis.stub(:load_to_cache)
另外,在某些我想测试的情况下,我只需要在相应的 Rspec 测试用例的 before 块中取消它们。
我知道您的 after_create
中可能发生了一些更复杂的事情,或者可能觉得这不是很优雅。您可以尝试取消模型中定义的回调,方法是在 Factory 中定义一个 after_create
挂钩(请参阅 factory_girl 文档),根据“取消回调”,您可以在其中定义相同的回调并返回 false
' 此 article 的部分。 (我不确定回调的执行顺序,这就是我没有选择这个选项的原因)。
最后,(对不起,我找不到这篇文章)Ruby 允许您使用一些肮脏的元编程来解开回调挂钩(您必须重置它)。我想这将是最不受欢迎的选择。
好吧,还有一件事,并不是真正的解决方案,但看看你是否可以在你的规范中使用 Factory.build,而不是实际创建对象。 (如果可以的话,将是最简单的)。
我发现以下解决方案是一种更简洁的方法,因为回调是在类级别运行/设置的。
# 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
关于上面发布的答案,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
这样您就不需要同时查看工厂和测试文件来了解测试的行为。
这是我创建的一个片段,用于以通用方式处理此问题。
它将跳过每个配置的回调,包括与 Rails 相关的回调,如 before_save_collection_association
,但它不会跳过一些使 ActiveRecord 正常工作所需的内容,如自动生成的 { 2} 回调。
# 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,所以看看测试日志你真正跳过了什么。也许你有一个 gem 添加一个你真正需要的回调,它会让你的测试惨遭失败,或者从你的 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
我有一个熟悉的问题,只有当我从 FactoryBot
创建记录时才想跳过回调,而此处发布的答案并没有解决我的问题,所以我找到了自己的解决方案,我将其发布在这里,所以可能会有用为别人。
班级
class User < ApplicationRecord
before_save :verify
end
工厂
FactoryBot.define do
factory :user do
transient do
skip_verify_callback { true }
end
before(:create) do |user, evaluator|
user.class.skip_callback(:save, :before, :verify) if evaluator.skip_verify_callback
end
after(:create) do |user, evaluator|
user.class.set_callback(:save, :before, :verify) if evaluator.skip_verify_callback
end
end
end
注意:以上创建回调仅在 FactoryBot.create
之后运行,因此 FactoryBot.build
不会触发这些回调。
我将工厂的默认行为设置为跳过验证回调,而我仍然可以通过使用如下参数创建用户来防止这种情况发生:
FactoryBot.create(:user, skip_verify_callback: false)
我认为这种方法更安全,因为 FactoryBot.create
立即开始和结束,我们不会有任何跳过回调的副作用。
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
您可以在需要运行时为这些实例设置带有特征的回调。
:on => :create
验证,请使用after(:build) { |user| user.class.skip_callback(:validate, :create, :after, :run_something) }
Class.skip_callback
调用将在其他测试中持续存在,因此如果您的其他测试期望回调发生,那么如果您尝试反转跳过回调逻辑,它们将失败。after(:build)
块中使用 Mocha 存根的答案。这让您的工厂默认运行回调,并且不需要在每次使用后重置回调。