跳过Factory Girl和Rspec的回调

时间:2012-01-05 23:02:35

标签: ruby-on-rails rspec factory-bot

我正在测试一个带有后创建回调的模型,我想在测试时只在某些情况下运行。如何从工厂跳过/运行回调?

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

16 个答案:

答案 0 :(得分:104)

我不确定它是否是最佳解决方案,但我已成功实现此目的:

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)

答案 1 :(得分:81)

如果您不想运行回调,请执行以下操作:

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

答案 2 :(得分:32)

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

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

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

答案 3 :(得分:24)

我想改进@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

答案 4 :(得分:6)

此解决方案适用于我,您无需在工厂定义中添加其他块:

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

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

答案 5 :(得分:5)

在Rspec 3中,一个简单的存根最适合我

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

答案 6 :(得分:4)

从我的工厂调用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

答案 7 :(得分:4)

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

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

答案 8 :(得分:3)

这将适用于当前的rspec语法(截至本文),并且更加清晰:

before do
   User.any_instance.stub :run_something
end

答案 9 :(得分:3)

詹姆斯·谢瓦利尔关于如何跳过before_validation回调的回答并没有帮助我,所以如果你和我一样在这里工作解决方案:

模特中的

before_validation :run_something, on: :create

在工厂:

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

答案 10 :(得分: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测试用例的前一块中取消它们。

我知道你的after_create可能会发生更复杂的事情,或者可能觉得这不太优雅。您可以尝试取消模型中定义的回调,方法是在工厂中定义after_create挂钩(请参阅factory_girl文档),您可以在其中定义相同的回调并返回false,根据这个article的“取消回调”部分。 (我不确定执行回调的顺序,这就是我没有选择此选项的原因。)

最后,(抱歉,我无法找到这篇文章)Ruby允许你使用一些脏的元编程解开一个回调钩子(你必须重置它)。我想这将是最不受欢迎的选择。

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

答案 11 :(得分: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

通过这种方式,您无需查看工厂文件和测试文件即可了解测试的行为。

答案 12 :(得分:1)

第5步-skip_callback从FactoryBot工厂跳过时引发参数错误。

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) }

随时将其与您喜欢的其他策略结合使用。

答案 13 :(得分:0)

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

# 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

答案 14 :(得分:0)

这是我创建的用于以一般方式处理此代码的代码段。
它将跳过配置的每个回调,包括与Rails相关的回调,例如 before_save_collection_association,但不会跳过使ActiveRecord正常运行所需的一些操作,例如自动生成的autosave_associated_records_for_回调。

{
    gigs:data[0], // <----- gigs
    gigs:data[1]  // <----- gigs
}

然后再这样:

{
    gigs:data[0],
    gigs2:data[1]  // <----- Change this key name gigs2
}

// OR

{
     gigs: [ ...data[0] , ...data[1] ] // <--- Merge result into one
}

YMMV不用说,因此请查看测试日志中您真正跳过的内容。也许您有一个gem添加了您真正需要的回调,它将使您的测试失败,或者从您的100个回调胖模型中,您只需要几个就可以进行特定的测试。在这种情况下,请尝试瞬态# 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)

奖金

有时您还需要跳过验证(所有这些都是为了使测试更快),然后尝试:

:force_callbacks

答案 15 :(得分:-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

您可以在需要运行时为这些实例设置回调。