【问题标题】:How to test for (ActiveRecord) object equality如何测试(ActiveRecord)对象相等性
【发布时间】:2011-06-11 22:14:07
【问题描述】:

Rails 3.0.3 上的Ruby 1.9.2 中,我正在尝试测试两个Friend(类继承自ActiveRecord::Base)对象之间的对象相等性。

对象相等,但测试失败:

Failure/Error: Friend.new(name: 'Bob').should eql(Friend.new(name: 'Bob'))

expected #<Friend id: nil, event_id: nil, name: 'Bob', created_at: nil, updated_at: nil>
     got #<Friend id: nil, event_id: nil, name: 'Bob', created_at: nil, updated_at: nil>

(compared using eql?)

只是为了笑,我还测试了对象身份,正如我所料,它失败了:

Failure/Error: Friend.new(name: 'Bob').should equal(Friend.new(name: 'Bob'))

expected #<Friend:2190028040> => #<Friend id: nil, event_id: nil, name: 'Bob', created_at: nil, updated_at: nil>
     got #<Friend:2190195380> => #<Friend id: nil, event_id: nil, name: 'Bob', created_at: nil, updated_at: nil>

Compared using equal?, which compares object identity,
but expected and actual are not the same object. Use
'actual.should == expected' if you don't care about
object identity in this example.

有人可以向我解释为什么第一个对象相等性测试失败,以及我如何才能成功地断言这两个对象相等吗?

【问题讨论】:

    标签: ruby-on-rails ruby activerecord identity equality


    【解决方案1】:

    如果您像我一样正在寻找对这个问题的 Minitest 答案,那么这里有一个自定义方法,它断言两个对象的属性是相等的。

    它假定您始终希望排除 idcreated_atupdated_at 属性,但您可以根据需要覆盖该行为。

    我喜欢保持我的test_helper.rb 干净,因此创建了一个包含以下内容的test/shared/custom_assertions.rb 文件。

    module CustomAssertions
      def assert_attributes_equal(original, new, except: %i[id created_at updated_at])
        extractor = proc { |record| record.attributes.with_indifferent_access.except(*except) }
        assert_equal extractor.call(original), extractor.call(new)
      end
    end
    

    然后更改您的 test_helper.rb 以包含它,以便您可以在测试中访问它。

    require 'shared/custom_assertions'
    
    class ActiveSupport::TestCase
      include CustomAssertions
    end
    

    基本用法:

    test 'comments should be equal' do
      assert_attributes_equal(Comment.first, Comment.second)
    end
    

    如果您想覆盖它忽略的属性,则使用 except arg 传递字符串或符号数组:

    test 'comments should be equal' do
      assert_attributes_equal(
        Comment.first, 
        Comment.second, 
        except: %i[id created_at updated_at edited_at]
      )
    end
    

    【讨论】:

      【解决方案2】:

      我在 RSpec 上创建了一个匹配器,只是为了进行这种类型的比较,非常简单,但是很有效。

      在这个文件里面: spec/support/matchers.rb

      你可以实现这个匹配器...

      RSpec::Matchers.define :be_a_clone_of do |model1|
        match do |model2|
          ignored_columns = %w[id created_at updated_at]
          model1.attributes.except(*ignored_columns) == model2.attributes.except(*ignored_columns)
        end
      end
      

      之后,您可以在编写规范时使用它,通过以下方式...

      item = create(:item) # FactoryBot gem
      item2 = item.dup
      
      expect(item).to be_a_clone_of(item2)
      # True
      

      有用的链接:

      https://relishapp.com/rspec/rspec-expectations/v/2-4/docs/custom-matchers/define-matcher https://github.com/thoughtbot/factory_bot

      【讨论】:

        【解决方案3】:
         META = [:id, :created_at, :updated_at, :interacted_at, :confirmed_at]
        
         def eql_attributes?(original,new)
           original = original.attributes.with_indifferent_access.except(*META)
           new = new.attributes.symbolize_keys.with_indifferent_access.except(*META)
           original == new
         end
        
         eql_attributes? attrs, attrs2
        

        【讨论】:

          【解决方案4】:

          查看==(别名eql?)操作上的API docsActiveRecord::Base

          如果 compare_object 是同一个确切的对象,或者 compare_object 是相同的类型并且 self 有一个 ID 并且它等于 comparison_object.id,则返回 true。

          请注意,根据定义,新记录不同于任何其他记录,除非其他记录是接收者本身。此外,如果您使用 select 获取现有记录并将 ID 省略,则您只能靠自己,此谓词将返回 false。

          还要注意,销毁记录会在模型实例中保留其 ID,因此删除的模型仍然具有可比性。

          【讨论】:

          • 更新了 Rails 3.2.8 的 API 文档链接 rubydoc.info/docs/rails/3.2.8/frames 另外,值得注意的是 eql? 已被覆盖,但别名 equal? 仍可与 object_id 进行比较
          • 这确实是比当前标记的正确答案更好的答案。 == 的文档解释了我需要了解的所有要点,以了解 rails 如何测试对象相等性。
          【解决方案5】:

          如果您想根据属性比较两个模型实例,您可能希望从比较中排除某些不相关的属性,例如:idcreated_at 和 @987654325 @。 (我认为这些是关于记录的更多元数据,而不是记录数据本身的一部分。)

          当您比较两个新的(未保存的)记录时,这可能无关紧要(因为 idcreated_atupdated_at 在保存之前都将是 nil),但我有时发现有必要比较一个已保存 对象和一个 未保存 对象(在这种情况下 == 会给你 false,因为 nil != 5)。或者我想比较两个 saved 对象以了解它们是否包含相同的 data (因此 ActiveRecord == 运算符不起作用,因为如果它们返回 false有不同的id,即使它们在其他方面相同)。

          我对这个问题的解决方案是在您希望使用属性进行比较的模型中添加类似这样的内容:

            def self.attributes_to_ignore_when_comparing
              [:id, :created_at, :updated_at]
            end
          
            def identical?(other)
              self. attributes.except(*self.class.attributes_to_ignore_when_comparing.map(&:to_s)) ==
              other.attributes.except(*self.class.attributes_to_ignore_when_comparing.map(&:to_s))
            end
          

          然后在我的规范中,我可以写出这样可读和简洁的东西:

          Address.last.should be_identical(Address.new({city: 'City', country: 'USA'}))
          

          我计划分叉 active_record_attributes_equality gem 并将其更改为使用此行为,以便更容易重用。

          不过,我的一些问题包括:

          • 这样的宝石已经存在了吗??
          • 应该调用什么方法?我不认为覆盖现有的== 运算符是一个好主意,所以现在我称它为identical?。但也许像 practically_identical?attributes_eql? 这样的东西会更准确,因为它不会检查它们是否严格相同(一些属性允许不同.)...
          • attributes_to_ignore_when_comparing 太冗长了。并不是说如果他们想使用 gem 的默认值,就需要将其显式添加到每个模型中。可能允许使用 ignore_for_attributes_eql :last_signed_in_at, :updated_at 之类的宏覆盖默认值

          欢迎评论...

          更新:我没有分叉active_record_attributes_equality,而是编写了一个全新的gem,active_record_ignored_attributes,可在http://github.com/TylerRick/active_record_ignored_attributeshttp://rubygems.org/gems/active_record_ignored_attributes 获得

          【讨论】:

            【解决方案6】:

            Rails 故意将相等检查委托给标识列。如果您想知道两个 AR 对象是否包含相同的内容,请比较在两者上调用 #attributes 的结果。

            【讨论】:

            • 但在这种情况下,两个实例的标识列都是nil,因为两者都没有保存。 eql?() 检查属性的值和类型。 nil.class == nil.classtruenil == niltrue,所以 OP 的第一个例子应该仍然是 true。您的回答没有解释为什么它返回 false。
            • 它不只是盲目地比较 id,它只比较 id 是否有意义。正如 Andy Lindeman 的回答中提到的:“根据定义,新记录不同于任何其他记录”。