关于ruby on rails:在Factory Girl和Rspec上跳过回调

Skip callbacks on Factory Girl and Rspec

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

1
2
3
4
class User < ActiveRecord::Base
  after_create :run_something
  ...
end

厂:

1
2
3
4
5
6
7
8
9
10
11
FactoryGirl.define do
  factory :user do
    first_name"Luiz"
    last_name"Branco"
    ...
    # skip callback

    factory :with_run_something do
      # run callback
  end
end

我不确定这是否是最好的解决方案,但是我已经成功实现了以下目标:

1
2
3
4
5
6
7
8
9
10
11
12
13
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

在没有回调的情况下运行:

1
FactoryGirl.create(:user)

使用回调运行:

1
FactoryGirl.create(:user_with_run_something)


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

1
2
User.skip_callback(:create, :after, :run_something)
Factory.create(:user)

请注意,skip_callback在运行后将在其他规范中保持不变,因此请考虑以下内容:

1
2
3
4
5
6
7
before do
  User.skip_callback(:create, :after, :run_something)
end

after do
  User.set_callback(:create, :after, :run_something)
end


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

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

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


我想对@luizbranco的答案进行改进,以在创建其他用户时使after_save回调更可重用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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回调的情况下运行:

1
FactoryGirl.create(:user)

使用after_save回调运行:

1
FactoryGirl.create(:user, :with_after_save_callback)

在我的测试中,我更喜欢默认情况下创建不带回调的用户,因为使用的方法运行的是我在测试示例中通常不想要的东西。

----------更新------------
我停止使用skip_callback,因为测试套件中存在一些不一致的问题。

替代解决方案1(使用存根和存根):

1
2
3
4
5
6
7
8
9
10
11
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(我的首选方法):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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


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

1
2
3
4
user = FactoryGirl.build(:user)
user.send(:create_without_callbacks) # Skip callback

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

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

1
ArgumentError: After commit callback :whatever_callback has not been defined

Rails 5中有一个更改,其中skip_callback如何处理无法识别的回调:

ActiveSupport::Callbacks#skip_callback now raises an ArgumentError if an unrecognized callback is remove

从工厂调用skip_callback时,AR模型中的实际回调尚未定义。

如果您已尝试了所有步骤并像我一样拉了头发,这是您的解决方案(可从搜索FactoryBot问题中获得)(注意raise: false部分):

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

随意将其与您喜欢的任何其他策略一起使用。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
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

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


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

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


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

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

当我在工厂中尝试过skip_callbacks时,即使我不使用工厂直接创建文档对象时,它仍然保留了回调跳过。因此,相反,我在after build调用中使用了mocha存根,并且一切运行正常:

1
2
3
4
5
6
7
8
9
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


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

在模型中:

1
before_validation :run_something, on: :create

在工厂:

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


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

1
2
3
before do
   User.any_instance.stub :run_something
end


就我而言,我有回调将某些内容加载到我的Redis缓存中。但是后来我没有/想要在我的测试环境中运行redis实例。

1
2
3
4
5
after_create :load_to_cache

def load_to_cache
  Redis.load_to_cache
end

对于我的情况,与上面类似,我只是在spec_helper中添加了load_to_cache方法,
与:

1
Redis.stub(:load_to_cache)

另外,在某些情况下,我想对此进行测试,我只需要在相应的Rspec测试用例的before模块中将它们取消存根即可。

我知道您在after_create中可能会发生一些更复杂的事情,或者可能不会觉得这很优雅。您可以尝试通过在Factory中定义一个after_create钩子(请参阅factory_girl docs)来取消模型中定义的回调,根据"取消回调",您可以在其中定义相同的回调并返回false。"部分。 (我不确定执行回调的顺序,这就是为什么我不选择此选项的原因)。

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

好吧,还有一件事,不是真正的解决方案,而是看看您是否可以按照自己的规格使用Factory.build,而不是实际创建对象。 (如果可以的话,将是最简单的)。


关于上面发布的答案,https://stackoverflow.com/a/35562805/2001785,您不需要将代码添加到工厂。我发现更容易在规范本身中重载方法。例如,而不是(与引用的文章中的工厂代码一起)

1
let(:user) { FactoryGirl.create(:user) }

我喜欢使用(没有引用的工厂代码)

1
2
3
4
5
6
7
let(:user) do
  FactoryGirl.build(:user).tap do |u|
      u.define_singleton_method(:send_welcome_email){}
      u.save!
    end
  end
end

这样,您无需查看工厂文件和测试文件即可了解测试的行为。


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 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

然后再:

1
create(:user, :skip_all_callbacks)

不用说,YMMV,因此请查看测试日志中您真正跳过的内容。也许您有一个gem添加了您真正需要的回调,它将使您的测试失败,或者从您的100个回调胖模型中,您只需要几个就可以进行特定的测试。对于这些情况,请尝试使用瞬态:force_callbacks

1
create(:user, :skip_all_callbacks, force_callbacks: [:some_important_callback])

奖金

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

1
2
3
  trait :skip_validate do
    to_create { |instance| instance.save(validate: false) }
  end

1
2
3
4
5
6
7
8
9
10
11
12
13
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

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