跳过 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个回答

3

詹姆斯·谢瓦利耶有关如何跳过 before_validation 回调的答案对我没有帮助,所以如果你和我一样遇到了同样的问题,这里提供一个可行的解决方案:

在模型中:

before_validation :run_something, on: :create

在工厂中:

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

3
我认为最好避免这种做法。它会跳过类的每个实例(不仅仅是工厂生成的实例)的回调函数。这会导致一些规范执行问题(例如在初始工厂构建之后禁用),这些问题可能很难调试。如果这是支持中所需的行为,应该明确地使用 Model.skip_callback(...) - Kevin Sylvestre

3
这将与当前rspec语法(截至本帖发布时)配合使用,更加简洁:
before do
   User.any_instance.stub :run_something
end

这在 Rspec 3 中已经被弃用。使用常规的存根对我有用,可以查看我的下面的答案。 - samg

2
我发现以下解决方案更加简洁,因为回调是在类级别上运行/设置的。
# 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

2
关于上面发布的答案,https://dev59.com/IWoy5IYBdhLWcg3wJKoq#35562805,您不需要将代码添加到工厂中。我发现在规范本身中重载方法更容易。例如,与引用帖子中的工厂代码相比,可以这样做:
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

这样你就不需要查看工厂和测试文件来理解测试的行为。

2
在我的情况下,回调函数会将某些内容加载到我的 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,参考本 文章 的“取消回调”部分。(我不确定回调的执行顺序,所以我没有选择这个选项)。

最后,(抱歉我找不到那篇文章)Ruby 允许你使用一些脏的元编程来解除回调钩子(你将不得不重置它)。我猜这将是最不受欢迎的选项。

好吧,还有一件事,不是真正的解决方案,但看看你是否可以在你的规格中使用 Factory.build,而不是实际创建对象。(如果你能这样做,那就是最简单的方法)。


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,所以看一下测试日志,你真正跳过了什么。也许你有一个添加了回调的宝石,你真的需要它,它会使你的测试失败或者从你的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

0

我遇到了一个熟悉的问题,我想在使用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开始和结束都很快,我们不会有任何跳过回调的副作用。


-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

当您想要运行它的这些实例时,您可以使用trait设置回调函数。


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