跳过关于Factory Girl和Rspec的回调


103

我正在使用创建后回调回调来测试模型,该回调仅在测试时仅在某些情况下运行。如何跳过/运行工厂的回调?

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

Answers:


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) }
James Chevalier,

7
反转跳过的回调逻辑会更好吗?我的意思是,默认值应该是在创建对象时触发回调,并且在特殊情况下应使用其他参数。因此FactoryGirl.create(:user)应该创建触发回调的用户,而FactoryGirl.create(:user_without_callbacks)应该创建没有回调的用户。我知道这只是一个“设计”修改,但是我认为这可以避免破坏现有的代码,并使代码更加一致。
Gnagno

3
正如@Minimal的解决方案所指出的,该Class.skip_callback调用将在其他测试之间保持不变,因此,如果您的其他测试希望发生回调,则在您尝试反转跳过的回调逻辑时它们将失败。
mpdaugherty 2014年

我最终使用了@uberllama的答案,关于在after(:build)块中与Mocha存根。这使您的出厂默认设置为运行回调,并且不需要在每次使用后重置回调。
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 2014年

我也喜欢这个 我不希望我的工厂永久改变行为。我想跳过它进行特定的测试。
theUtherSide

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 '18

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


RuboCop抱怨“替代解决方案2”的“样式/单行方法:避免单行方法定义”,因此我需要更改格式,但否则就太完美了!
coberlin '18

14

Rails 5- skip_callback从FactoryBot工厂跳过时引发Argument错误。

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

此解决方案对我有效,您无需在Factory定义中添加其他块:

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

重要说明, 您应同时指定它们两者。如果仅在之前使用并运行多个规范,它将尝试多次禁用回调。它将第一次成功,但是在第二次,将不再定义回调。所以会出错


这在最近的项目的套件中造成了一些模糊的失败-我有类似于@Sairam的答案,但是在两次测试之间的类中未设置回调。哎呀
kfrz

4

从我的工厂调用skip_callback对我来说是有问题的。

就我而言,我在创建之前和之后都有一个文档类,其中包含一些与s3相关的回调,我只想在需要测试整个堆栈时才运行。否则,我想跳过那些s3回调。

当我在工厂中尝试过skip_callbacks时,即使我直接创建文档对象而无需使用工厂,它也保留了回调跳过。因此,相反,我在after build调用中使用了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钩子的解决方案(尝试skip_callback与任何FactoryGirl的解决方案beforeafter选项结合使用buildcreate但都无法使用)
Mike T

3

这将与当前的rspec语法一起使用(截至本文),并且更加简洁:

before do
   User.any_instance.stub :run_something
end

Rspec 3中不推荐使用此方法。使用常规存根为我工作,请参阅下面的答案。
samg 2014年

3

James Chevalier关于如何跳过before_validation回调的答案对我没有帮助,因此,如果您遇到与我相同的问题,这是可行的解决方案:

在模型中:

before_validation :run_something, on: :create

在工厂:

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

2
我认为最好避免这种情况。它跳过该类的每个实例的回调(不仅仅是工厂女孩生成的回调)。这将导致某些规范执行问题(即,如果在初始工厂建立之后就禁用了),则可能难以调试。如果这是规范/支持中所需的行为,则应明确进行以下操作: Model.skip_callback(...)
Kevin Sylvestre

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允许您使用一些脏的元编程来取消回调钩子(您必须将其重置)。我想这将是最不受欢迎的选择。

好吧,还有另外一件事,不是真正的解决方案,而是看看您是否可以按照自己的规格使用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,但不会跳过使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

-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

您可以在要运行的时候为这些实例设置一个带有特征的回调。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.