如何测试(ActiveRecord)对象的相等性

时间:2011-01-19 17:18:36

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

Ruby 1.9.2 Rails 3.0.3上,我正在尝试测试两个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.

有人可以向我解释为什么对象相等的第一次测试失败,以及我如何成功断言这两个对象是否相等?

6 个答案:

答案 0 :(得分:44)

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

答案 1 :(得分:40)

查看==的{​​{1}}(别名eql?)操作的API docs

  

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

     

请注意,根据定义,新记录与任何其他记录不同,除非另一条记录是接收方本身。此外,如果您使用select获取现有记录并将ID保留,您自己就可以,这个谓词将返回false。

     

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

答案 2 :(得分:20)

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

当您比较两个新的(未保存的)记录时,这可能无关紧要(因为idcreated_atupdated_at都将nil,直到保存为止),但我有时会发现有必要将保存的对象与未保存的对象进行比较(在这种情况下==会因为nil而给你错误!= 5)。或者我想比较两个保存的对象,以确定它们是否包含相同的数据(因此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_ignored_attributes ,而不是分支active_record_attributes_equalityhttp://github.com/TylerRick/active_record_ignored_attributes和{{3} }}

答案 3 :(得分:2)

 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 :(得分:2)

我仅为这种比较而在RSpec上创建了一个匹配器,非常简单,但是很有效。

在此文件中: `<div *ngIf="(error$ | async) !== (of({}) | async)"> <span>Something went wrong {{(error$ | async)?.errorResponse}}</span> </div>` `<div *ngIf="(error$ | async) != (of({}) | async)"> <span>Something went wrong {{(error$ | async)?.errorResponse}}</span> </div>` `<div *ngIf="(error$ | async) != '{}'"> <span>Something went wrong {{(error$ | async)?.errorResponse}}</span> </div>` `<div *ngIf="(error$ | async) != undefined"> <span>Something went wrong {{(error$ | async)?.errorResponse}}</span> </div>` `<div *ngIf="(error$ | async) != {}"> <span>Something went wrong {{(error$ | async)?.errorResponse}}</span> </div> `

您可以实现此匹配器...

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

有用的链接:

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

答案 5 :(得分:0)

如果像我一样,您正在寻找这个问题的 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