跳过 Factory Girl 和 Rspec 的回调函数。

125

我正在测试一个带有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
18个回答

120

我不确定这是否是最佳解决方案,但我已经成功地使用以下方法实现了:

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
14
将跳过回调的逻辑反转岂不更好?我的意思是,当我创建一个对象时,默认情况下应该触发回调,而我应该使用不同的参数来处理异常情况。因此,FactoryGirl.create(:user) 应该创建用户并触发回调,而 FactoryGirl.create(:user_without_callbacks) 则应创建用户但不触发回调。我知道这只是一种“设计”修改,但我认为这可以避免破坏先前存在的代码,并且更加一致。 - Gnagno
7
正如@Minimal的解决方案所指出的那样,Class.skip_callback调用将在其他测试中保持持久性,因此,如果您尝试反转跳过回调逻辑,则其他测试期望回调发生的话将失败。 - mpdaugherty
最终我采用了@uberllama的答案,使用Mocha在after(:build)块中进行存根。这样可以让你的工厂默认运行回调函数,而不需要在每次使用后重置回调函数。 - mpdaugherty
你有没有想过这个工作方式的其他方法?http://stackoverflow.com/questions/35950470/rails-factorygirl-trait-association-with-model-after-create-callback-not-setting - Chris Hough
我曾经对语法感到困惑,只是想指出如果模型中的回调是 after_save: my_method,那么在工厂中的语法就是 skip_callback(:save, :after, :my_method) - Michael Mudge

116

当您不想运行回调函数时,请执行以下操作:

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

17
我更喜欢这个答案,因为它明确指出跳过回调会在类级别上停留,并且在后续的测试中仍然会跳过回调。 - siannopollo
我也更喜欢这个。我不想让我的工厂永久性地表现出不同的行为。我想跳过一组特定的测试。 - theUtherSide
1
除了并行测试,您无法控制在清除之前是否会创建另一个对象。 - Ryan

57

这些解决方案都不好。它们通过从类中删除应该从实例中删除的功能来破坏了类。

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

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


2
我真的很喜欢这个答案,并且想知道是否应该将类似于此的内容作为 FactoryGirl 本身的一部分进行别名处理,以便意图能够立即清晰明了。 - Giuseppe
我也非常喜欢这个答案,以至于我会给其他所有答案投反对票,但是看起来我们需要将一个块传递给定义的方法,如果它是你的回调函数类似于 around_*(例如 user.define_singleton_method(:around_callback_method){|&b| b.call })。 - Quv
1
不仅是更好的解决方案,而且由于某种原因,另一种方法对我无效。当我实施它时,它会说没有回调方法存在,但当我将其留出时,它会要求我存根不必要的请求。虽然它引导我找到了一个解决方案,但有人知道这可能是为什么吗? - Babbz77

31

我想对@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,因为测试套件存在一些不一致的问题。

替代方案1(使用stub和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

你有没有想过这个工作方式的其他方法?http://stackoverflow.com/questions/35950470/rails-factorygirl-trait-association-with-model-after-create-callback-not-setting - Chris Hough
RuboCop 抱怨了“Style/SingleLineMethods: 避免使用单行方法定义”的替代方案2,所以我需要改变格式,但除此之外它是完美的! - coberlin

24

Rails 5 - skip_callback在跳过FactoryBot工厂时引发参数错误。

ArgumentError: After commit callback :whatever_callback has not been defined

Rails 5中有一个变化,关于如何处理未识别的回调函数:

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

6

在Rspec 3中,对我来说最好的方法是使用简单的存根。

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

5
你需要为 User 的实例设置它;:run_something 不是一个类方法。 - PJSCopeland

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

5

在我的工厂中调用skip_callback对我来说很棘手。

在我的情况下,我有一个文档类,在创建之前和之后会有一些与s3相关的回调函数,我只想在需要测试整个堆栈时运行它们。否则,我希望跳过这些s3回调。

当我在我的工厂中尝试使用skip_callbacks时,即使我直接创建一个文档对象而没有使用工厂,该回调跳过也会保持不变。因此,我在after build调用中使用了mocha stubs,并且一切都完美地工作:

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”选项进行“build”和“create”的“skip_callback”都无法正常工作)。 - Mike T

3
这是一个比较旧的问题,有一些好的答案,但是由于几个原因,它们都不适用于我。
  • 不想在运行时修改某个类的行为
  • 不想在我的类中到处使用attr_accessor,因为在模型内部只用于测试的逻辑似乎很奇怪
  • 不想将rspec before/after块的调用放在各种规范上来stub/unstub行为
使用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 进行各种调用时节省了测试套件。个人情况可能有所不同。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接