【问题标题】:Skip callbacks on Factory Girl and Rspec跳过 Factory Girl 和 Rspec 的回调
【发布时间】:2012-02-03 18:56:40
【问题描述】:

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

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

【问题讨论】:

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


    【解决方案1】:

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

    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)
    

    【讨论】:

    • 如果您想跳过:on =&gt; :create 验证,请使用after(:build) { |user| user.class.skip_callback(:validate, :create, :after, :run_something) }
    • 把跳过回调逻辑倒过来不是更好吗?我的意思是,默认应该是当我创建一个对象时触发回调,并且我应该为异常情况使用不同的参数。所以 FactoryGirl.create(:user) 应该创建触发回调的用户,而 FactoryGirl.create(:user_without_callbacks) 应该创建没有回调的用户。我知道这只是一个“设计”修改,但我认为这可以避免破坏现有代码,并且更加一致。
    • 正如@Minimal 的解决方案所述,Class.skip_callback 调用将在其他测试中持续存在,因此如果您的其他测试期望回调发生,如果您尝试反转跳过回调逻辑,它们将失败。
    • 我最终在after(:build) 块中使用了@uberllama 关于使用Mocha 存根的答案。这让您的工厂默认运行回调,并且不需要在每次使用后重置回调。
    • 你对这种方式有什么想法吗? stackoverflow.com/questions/35950470/…
    【解决方案2】:

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

    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
    

    【讨论】:

    • 我更喜欢这个答案,因为它明确指出跳过回调挂在类级别,因此将在后续测试中继续跳过回调。
    • 我也更喜欢这个。我不希望我的工厂永久地表现不同。我想跳过它进行一组特定的测试。
    【解决方案3】:

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

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

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

    【讨论】:

    • 我真的很喜欢这个答案,并且想知道这样的东西是否应该是 FactoryGirl 本身的一部分。
    • 我也非常喜欢这个答案,我会否决其他所有内容,但看来我们需要将一个块传递给定义的方法,如果它是您的回调是around_* 的亲属(例如@ 987654323@).
    • 不仅是一个更好的解决方案,而且由于某种原因,另一种方法对我不起作用。当我实现它时,它说不存在回调方法,但是当我把它排除在外时,它会要求我存根不必要的请求。虽然它会引导我找到解决方案,但有人知道为什么会这样吗?
    【解决方案4】:

    我想改进 @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(使用存根和非存根):

    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
    

    【讨论】:

    • 你对这种方式有什么想法吗? stackoverflow.com/questions/35950470/…
    • RuboCop 抱怨替代解决方案 2 的“样式/单行方法:避免单行方法定义”,所以我需要更改格式,否则它是完美的!
    【解决方案5】:

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

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

    【讨论】:

    • 太好了,这正是发生在我身上的事情。请注意,如果您删除了一次回调,然后再试一次,就会发生这种情况,因此很可能会为工厂多次触发。
    【解决方案6】:

    此解决方案适用于我,您不必在工厂定义中添加额外的块:

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

    【讨论】:

      【解决方案7】:
      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
      

      重要提示您应该同时指定它们。 如果只使用 before 并运行多个规范,它会尝试多次禁用回调。第一次它会成功,但第二次,回调将不再被定义。所以会报错

      【讨论】:

      • 这在最近的一个项目中的一个套件中导致了一些混淆失败 - 我有一些类似于@Sairam 的答案,但回调在测试之间的类中未设置。哎呀。
      【解决方案8】:

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

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

      【讨论】:

      • 你需要为Userinstances设置它; :run_something 不是类方法。
      【解决方案9】:

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

      【讨论】:

      • 在这里的所有解决方案中,并且为了在工厂中拥有逻辑,这是唯一一个可以使用 before_validation 钩子的解决方案(尝试使用 FactoryGirl 的任何 before 进行 skip_callbackafter buildcreate 的选项无效)
      【解决方案10】:

      这将适用于当前的 rspec 语法(截至本文)并且更简洁:

      before do
         User.any_instance.stub :run_something
      end
      

      【讨论】:

      • 这在 Rspec 3 中已被弃用。使用常规存根对我有用,请参阅下面的答案。
      【解决方案11】:

      James Chevalier 关于如何跳过 before_validation 回调的回答对我没有帮助,所以如果你和我一样,这里是可行的解决方案:

      在模型中:

      before_validation :run_something, on: :create
      

      在工厂:

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

      【讨论】:

      • 我认为最好避免这种情况。它跳过类的每个实例的回调(不仅仅是工厂女孩生成的那些)。这将导致一些难以调试的规范执行问题(即,如果禁用发生在初始工厂构建之后)。如果这是规范/支持中所需的行为,则应明确完成:Model.skip_callback(...)
      【解决方案12】:

      这是一个较老的问题,有一些很好的答案,但由于某些原因,它们都不适合我

      • 不喜欢在运行时修改某些类的行为的想法
      • 不想在我的所有课程中都使用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 进行的各种调用。 YMMV

      【讨论】:

        【解决方案13】:

        在我的情况下,我有回调将某些内容加载到我的 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 ' 这个article 的部分。 (我不确定回调的执行顺序,这就是我没有选择此选项的原因)。

        最后,(对不起,我找不到这篇文章)Ruby 允许您使用一些肮脏的元编程来解开回调挂钩(您必须重置它)。我想这将是最不受欢迎的选择。

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

        【讨论】:

          【解决方案14】:

          关于上面发布的答案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
          

          这样您就不需要同时查看工厂和测试文件来了解测试的行为。

          【讨论】:

            【解决方案15】:

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

            # 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
            

            【讨论】:

              【解决方案16】:

              这是我创建的一个 sn-p,用于以通用方式处理此问题。
              它将跳过每个配置的回调,包括与 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
              

              【讨论】:

                【解决方案17】:
                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
                

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

                【讨论】:

                  猜你喜欢
                  • 1970-01-01
                  • 2014-01-12
                  • 2012-06-12
                  • 1970-01-01
                  • 1970-01-01
                  • 2015-04-29
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  相关资源
                  最近更新 更多