【问题标题】:How to disable belongs_to :touch option in Rspec tests for Rails models?如何在 Rails 模型的 Rspec 测试中禁用 belongs_to :touch 选项?
【发布时间】:2013-09-20 17:47:54
【问题描述】:

拥有庞大的模型堆栈并广泛使用玩偶缓存技术,最终会在模型更新后“触及”许多父模型。

在测试时,这似乎是在浪费时间,除非您尝试专门测试该功能。

有没有办法防止模型在测试环境或测试级别与touch 关联belongs_to

更新 1:

我对这个案子的第一次尝试是

# /config/initializers/extensions.rb
#
class ActiveRecord::Base
  def self.without_touch_for_association(association_name, &block)
    association_name = association_name.to_sym
    association = self.reflect_on_all_associations(:belongs_to).select { |reflection| reflection.name == association_name }.first
    options = association.options
    association.instance_variable_set :@options, options.except(:touch)

    yield

    association.instance_variable_set :@options, options
  end
end

Post.without_touch_for_association(:user) do
  Post.last.save
end

当然,没有成功,保存Post.last 还是会碰到User

更新理由:

我理解并同意这种方法可能会导致错误,这根本不是一个好的做法。问题是我有一个包含大量集成和单元测试的巨大套件。娃娃缓存也深入模型树。每次查看日志时,我都会看到很大一部分与触摸相关的查询。我知道最好的方法是优化单元测试以添加更多的模拟和存根以及更少的持久性。在集成测试中解决问题更加困难。

无论如何,我问这个问题是为了学习和研究。我有兴趣探索这种技术的潜在速度改进。

解决方案:请参阅下面我自己的答案以获取工作代码。

【问题讨论】:

  • 对于未提供直接解决方案提前致歉。但是,我首先会非常谨慎地尝试这样做,因为它会显着改变您的应用程序的行为,并成为纯生产错误的丰富潜在来源。如果#touch 调用确实导致您的测试套件显着变慢,那么似乎一开始就创建了太多的持久记录。您主要关心单元测试还是集成测试?
  • 感谢 SimonC!我用我的理由更新了这个问题。干杯!
  • 好吧,我不认为这是一个很好的建议,但不能像这样的任何实例对 touch 方法进行存根:User.any_instance.stub(:touch).and_return(true) 所以它实际上并没有做任何事情?跨度>
  • 不。我指的是belongs_to :user, touch: true,我想在其中使touch 选项静音。

标签: ruby-on-rails rspec associations metaprogramming belongs-to


【解决方案1】:

假设您使用的是 Rails 4.1.4 或更高版本:

User.no_touching do
  Post.last.save
end

甚至

ActiveRecord::Base.no_touching do
  Post.last.save
end

ActiveRecord::NoTouching

【讨论】:

    【解决方案2】:

    我不同意为了测试目的而更改代码的想法。在我看来,测试应该是一个独立的程序。

    在我看来,您应该为您的测试套件提供一种方法,以便仅在某些情况下改变模型的行为。

    以下代码

    class Book < ActiveRecord::Base
      belongs_to :author, touch: true
    end
    
    class Author < ActiveRecord::Base
      has_many :books
    end
    

    这是你的情况将定义一个实例方法

    belongs_to_touch_after_save_or_destroy_for_author

    Book 课程的幕后花絮。 (感谢 AR http://apidock.com/rails/ActiveRecord/Associations/Builder/BelongsTo/add_touch_callbacks

    因此,在您的测试代码中,您可以覆盖该方法以执行不同的操作或根本不执行任何操作!

    在我的例子中,我将 Rspec 与 FactoryGirl 一起使用,所以我所做的是为 Book 类创建一个特殊的工厂特征,该类为该对象重新定义 belongs_to_touch_after_save_or_destroy_for_author

    FactoryGirl.define do
      factory :book do
        ...
        ...
      end
    
      trait :no_touch do
        before(:create) do |book_no_touch|
          def book_no_touch.belongs_to_touch_after_save_or_destroy_for_author
            true
          end
        end
      end
    end
    

    这样,当您需要测试与触摸相关对象无关的东西时,您可以使用该工厂创建一个书籍对象

    book = FactoryGirl.create(:book, :no_touch)

    【讨论】:

    • 是的!我在控制台上尝试了这个 sn-p:Post.send( :define_method, :belongs_to_touch_after_save_or_destroy_for_user) { true },它成功地禁用了回调。现在,对于 A:关于如何将这一点推广到所有 AR 类的任何想法,正如我所尝试的那样?
    • 嗯,也许是通过修补FactoryGirl的工厂?
    • 好吧,我最终在您的帮助下解决了这个问题。我发布了我的完整工作解决方案,并将其标记为最完整的解决方案,但功劳和赏金归于您,因为您将我指向 belongs_to_touch... 方法。谢谢!
    【解决方案3】:

    对于 Rails >= 4.2

    感谢@Dorian,在 Rails 4.2 中使用ActiveRecord::NoTouching

    适用于 Rails

    我在 rspec 支持文件中的工作代码:

    # /spec/support/active_record_extensions.rb
    class ActiveRecord::Base
      def self.without_touch_for_association(association, &block)
        method_name = :"belongs_to_touch_after_save_or_destroy_for_#{association}"
        return unless self.instance_methods.include?(method_name)
    
        method = self.send(:instance_method, method_name)
        self.send(:define_method, method_name) { true }
    
        yield
    
        self.send(:define_method, method_name, method)
        nil
      end
    
      def self.disable_touch_associations!
        associations = self.reflect_on_all_associations(:belongs_to)
        associations.each do |association|
          self.without_touch_for_association association.name do
            return
          end
        end
        nil
      end
    end
    

    将此添加到您的 ./spec/spec_helper.rb 以禁用对整个测试套件定义的任何模型的所有触摸调用:

    RSpec.configure do |config|
      if ENV['SILENCE_TOUCHES']
        config.before :suite do
          ActiveRecord::Base.descendants.each {|model| model.disable_touch_associations! }
        end
      end
    end
    

    在特定测试中暂时禁用模型和关联的触摸。

    Post.without_touch_for_association(:user) do
      Post.last.save
    end
    

    感谢下面的@xlembouras 为我指明了正确的方向!

    我在我们的测试中使用了这个功能,我注意到测试套件的速度降低了 25%,对于 30 分钟的测试套件。经过更深入的研究,我可能会发布更准确的结果。

    【讨论】:

    • 这和我说的完全不一样。你最终会修补你的ActiveRecord::Base 并在你的整个堆栈中添加额外的功能只是为了测试。所有这些都应该提取到您的工厂(首选)或规范中的 before 块,该块仅在需要时针对特定模型执行。
    • 嘿!你是对的。我实际上使用了稍后将代码添加到由环境变量控制的 spec_helper.rb 文件。我没有在答案中解释这一点,但我刚刚更新了它。感谢您指出这一点!
    • 顺便说一句,我很想在我的工厂中使用它,但是恕我直言,为每个工厂添加一个特征并不是一个好方法。我没有找到如何将这个位全局添加到所有工厂。
    • @xlembouras 我很好奇:为什么要投反对票?我相信这个答案在解决问题方面更完整。我可以接受你的,但你需要提供所要求的概括。无论如何,这个答案是正确的。
    • ActiveRecord::NoTouching 是正确的做法
    【解决方案4】:

    我不确定这是否可行,但您可以尝试以下方法:

    belongs_to :foo, touch: APP_CONFIG['doll_touch']
    

    其中 APP_CONFIG 是在此 guide 之后设置的应用程序参数。

    因此,在配置的生产/开发部分,您将doll_touch 设置为true,并在测试中设置为false

    【讨论】:

    • 嗯,这似乎应该可行。很好的尝试。但这需要将原始代码更改几十次......我仍然想了解如何对测试环境进行猴子补丁,我将等待有人回答这部分。如果没有,赏金就是你的了。
    • 别着急,相信你迟早会找到“猴子补丁”的解决方案。
    猜你喜欢
    • 1970-01-01
    • 2022-01-11
    • 1970-01-01
    • 1970-01-01
    • 2012-07-16
    • 2017-05-28
    • 2011-11-24
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多