Skip callbacks on Factory Girl and Rspec

I'm testing a model with an after create callback that I'd like to run only on some occasions while testing. How can I skip/run callbacks from a factory?

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

Factory:

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


factory :with_run_something do
# run callback
end
end
58799 次浏览

在我的例子中,我使用回调将某些内容加载到我的 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或可能不会发现这非常优雅。您可以尝试取消在您的模型中定义的回调,通过定义一个 after_create钩子在您的工厂(参考工厂 _ 女孩文档) ,在那里您可以定义一个相同的回调,并返回 false,根据这个 文章的“取消回调”部分。(我不确定回调的执行顺序,这就是为什么我没有选择这个选项)。

最后,(对不起,我找不到这篇文章) Ruby 允许您使用一些肮脏的元编程来解除回调钩子(您必须重置它)。我想这是最不可取的选择。

还有一件事,不是真正的解决方案,但是看看你是否可以在你的 specs 中使用 Factory.build,而不是实际创建对象。(如果可以的话,这将是最简单的方法)。

我不确定这是否是最好的解决方案,但我已经成功地实现了这一点,使用:

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

从我的工厂调用 Skip _ callback 对我来说是个问题。

在我的例子中,我有一个文档类,在创建之前和之后都有一些与 s3相关的回调,只有在需要测试完整堆栈时才想运行这些回调。否则,我想跳过这些 s3回调。

当我在工厂中尝试跳过 _ 回调时,它甚至在我不使用工厂直接创建文档对象时也会保持该回调跳过。所以,我在构建后的调用中使用了摩卡碎片,一切都很完美:

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

This will work with current rspec syntax (as of this post) and is much cleaner:

before do
User.any_instance.stub :run_something
end

这个解决方案适用于我,你不必添加一个额外的块到您的工厂定义:

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


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

我想对@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)

在我的测试中,我更喜欢创建没有默认回调的用户,因为使用的方法运行额外的东西,我通常不希望在我的测试示例。

————更新—————— 因为测试套件中存在一些不一致的问题,所以我停止使用 Skip _ callback。

Alternative Solution 1 (use of stub and unstub):

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

在 Rspec 3中,一个简单的存根对我来说效果最好

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

这些解决方案没有一个是好的。它们通过删除应该从实例而不是从类中删除的功能来破坏类。

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

我没有抑制回调,而是抑制了回调的功能。在某种程度上,我更喜欢这种方法,因为它更加明确。

James Chevalier 关于如何跳过审核前回调的回答对我没有帮助,所以如果你像我一样在这里掉队,这是一个可行的解决方案:

in model:

before_validation :run_something, on: :create

in factory:

after(:build) { |obj| obj.class.skip_callback(:validation, :before, :run_something) }
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

当您想要运行这些实例时,您可以只用一个 trait 来设置回调。

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 并运行多个 specs,它将尝试多次禁用回调。第一次会成功,但第二次就不再定义回调了。所以它会出错

我发现下面的解决方案是一种更干净的方法,因为回调是在类级别上运行/设置的。

# 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

Regarding the answer posted above, https://stackoverflow.com/a/35562805/2001785, you do not need to add the code to the factory. I found it easier to overload the methods in the specs themselves. For example, instead of (in conjunction with the factory code in the cited post)

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 5-skip_callback在从 FactoryBot 工厂跳过时引发参数错误。

ArgumentError: After commit callback :whatever_callback has not been defined

有一个 更改 Rails 5,它介绍了 Skip _ callback 如何处理无法识别的回调:

如果删除了无法识别的回调,回调 # Skip _ callback 现在会引发 ArgumentError

当从工厂调用 skip_callback时,AR 模型中的实际回调还没有定义。

如果你已经尝试了所有的方法,像我一样把头发揪出来,这里是你的解决方案 (通过搜索 FactoryBot 问题得到的)(注意 raise: false部分) :

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

不管你喜欢什么策略,都可以随意使用。

下面是我创建的以通用方式处理此问题的代码片段。
它将跳过所有配置的回调函数,包括与 Rails 相关的回调函数,如 before_save_collection_association ,但是它不会跳过使 ActiveRecord 正常工作所需的一些内容,比如自动生成的 autosave_associated_records_for_回调。

# 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

这是一个比较古老的问题,有一些很好的答案,但是由于一些原因,它们中没有一个对我很有用

  • didn't like the idea of modifying the behavior of some class at runtime
  • 我不想在整个课程中都使用 attr_accessor,因为把逻辑只用于模型内部的测试看起来很奇怪
  • 不想调用各种规格的 stub/unstub行为的 rspec before/after

使用 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

我有一个熟悉的问题,我想跳过回调只有当我创建一个记录从 FactoryBot和答案张贴在这里没有解决我的问题,所以我找到了自己的解决方案,我张贴在这里,所以可能会有用的其他人。

Class

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在瞬间开始和结束,我们不会有跳过回调的任何副作用。