How to test for (ActiveRecord) object equality

Ruby on-RailsRubyActiverecordIdentityEquality

Ruby on-Rails Problem Overview


In Ruby 1.9.2 on Rails 3.0.3, I'm attempting to test for object equality between two Friend (class inherits from ActiveRecord::Base) objects.

The objects are equal, but the test fails:

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?)

Just for grins, I also test for object identity, which fails as I'd expect:

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.

Can someone explain to me why the first test for object equality fails, and how I can successfully assert those two objects are equal?

Ruby on-Rails Solutions


Solution 1 - Ruby on-Rails

Rails deliberately delegates equality checks to the identity column. If you want to know if two AR objects contain the same stuff, compare the result of calling #attributes on both.

Solution 2 - Ruby on-Rails

Take a look at the http://apidock.com/rails/v3.2.13/ActiveResource/Base/%3D%3D">API docs on the == (alias eql?) operation for ActiveRecord::Base

> Returns true if comparison_object is the same exact object, or comparison_object is of the same type and self has an ID and it is equal to comparison_object.id. > >Note that new records are different from any other record by definition, unless the other record is the receiver itself. Besides, if you fetch existing records with select and leave the ID out, you’re on your own, this predicate will return false. > >Note also that destroying a record preserves its ID in the model instance, so deleted models are still comparable.

Solution 3 - Ruby on-Rails

If you want to compare two model instances based on their attributes, you will probably want to exclude certain irrelevant attributes from your comparison, such as: id, created_at, and updated_at. (I would consider those to be more metadata about the record than part of the record's data itself.)

This might not matter when you are comparing two new (unsaved) records (since id, created_at, and updated_at will all be nil until saved), but I sometimes find it necessary to compare a saved object with an unsaved one (in which case == would give you false since nil != 5). Or I want to compare two saved objects to find out if they contain the same data (so the ActiveRecord == operator doesn't work, because it returns false if they have different id's, even if they are otherwise identical).

My solution to this problem is to add something like this in the models that you want to be comparable using attributes:

  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

Then in my specs I can write such readable and succinct things as this:

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

I'm planning on forking the active_record_attributes_equality gem and changing it to use this behavior so that this can be more easily reused.

Some questions I have, though, include:

  • Does such a gem already exist??
  • What should the method be called? I don't think overriding the existing == operator is a good idea, so for now I'm calling it identical?. But maybe something like practically_identical? or attributes_eql? would be more accurate, since it's not checking if they're strictly identical (some of the attributes are allowed to be different.)...
  • attributes_to_ignore_when_comparing is too verbose. Not that this will need to be explicitly added to each model if they want to use the gem's defaults. Maybe allow the default to be overridden with a class macro like ignore_for_attributes_eql :last_signed_in_at, :updated_at

Comments are welcome...

Update: Instead of forking the active_record_attributes_equality, I wrote a brand-new gem, active_record_ignored_attributes, available at http://github.com/TylerRick/active_record_ignored_attributes and http://rubygems.org/gems/active_record_ignored_attributes

Solution 4 - Ruby on-Rails

 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

Solution 5 - Ruby on-Rails

I created a matcher on RSpec just for this type of comparison, very simple, but effective.

Inside this file: spec/support/matchers.rb

You can implement this matcher...

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

After that, you can use it when writing a spec, by the following way...

item = create(:item) # FactoryBot gem
item2 = item.dup

expect(item).to be_a_clone_of(item2)
# True

Useful links:

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

Solution 6 - Ruby on-Rails

If like me you're looking for a Minitest answer to this question then here's a custom method that asserts that the attributes of two objects are equal.

It assumes that you always want to exclude the id, created_at, and updated_at attributes, but you can override that behaviour if you wish.

I like to keep my test_helper.rb clean so created a test/shared/custom_assertions.rb file with the following content.

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

Then alter your test_helper.rb to include it so you can access it within your tests.

require 'shared/custom_assertions'

class ActiveSupport::TestCase
  include CustomAssertions
end

Basic usage:

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

If you want to override the attributes it ignores then pass an array of strings or symbols with the 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

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionryonlifeView Question on Stackoverflow
Solution 1 - Ruby on-RailsnoodlView Answer on Stackoverflow
Solution 2 - Ruby on-RailsAndy LindemanView Answer on Stackoverflow
Solution 3 - Ruby on-RailsTyler RickView Answer on Stackoverflow
Solution 4 - Ruby on-RailsscottView Answer on Stackoverflow
Solution 5 - Ruby on-RailsVictorView Answer on Stackoverflow
Solution 6 - Ruby on-RailsTom LuceView Answer on Stackoverflow