Как проверить на (ActiveRecord) равенство объектов - PullRequest
49 голосов
/ 19 января 2011

В 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.

Может кто-нибудь объяснить мне, почему первый тест для объектаравенство не удается, и как я могу успешно утверждать, что эти два объекта равны?

Ответы [ 5 ]

44 голосов
/ 19 января 2011

Rails намеренно делегирует проверки на равенство в столбец идентификаторов. Если вы хотите узнать, содержат ли два объекта AR один и тот же материал, сравните результат вызова #attributes для обоих.

40 голосов
/ 19 января 2011

Посмотрите API документы на операции == (псевдоним eql?) для ActiveRecord::Base

Возвращает значение true, если сравнение_объекта является одним и тем же точным объектом, или сравнение_объекта того же типа, а self имеет идентификатор, и оно равно сравнение_объекта.

Обратите внимание, что новые записи отличаются от любой другой записи по определению , если только другая запись не является самим получателем. Кроме того, если вы выбираете существующие записи с помощью select и пропускаете идентификатор, вы сами по себе, этот предикат вернет false.

Обратите внимание, что при удалении записи сохраняется ее идентификатор в экземпляре модели, поэтому удаленные модели по-прежнему сравнимы.

20 голосов
/ 17 августа 2011

Если вы хотите сравнить два экземпляра модели на основе их атрибутов, вы, вероятно, захотите исключить некоторые несущественные атрибуты из вашего сравнения, такие как: id, created_at и updated_at,(Я бы посчитал, что это больше метаданных о записи, чем части самих данных записи.)

Это может не иметь значения при сравнении двух новых (несохраненных) записей (так как id, created_at и updated_at будут 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 и изменить его, чтобы использовать это поведение так, чтобы это могло бытьлегко использовать повторно.

Некоторые вопросы, которые у меня есть, включают:

  • Такой камень уже существует ??
  • Как должен называться метод?Я не думаю, что переопределение существующего оператора == является хорошей идеей, поэтому сейчас я называю его identical?.Но, возможно, что-то вроде practically_identical? или attributes_eql? будет более точным, поскольку он не проверяет, являются ли они строго идентичными ( некоторым атрибутам разрешено быть разными.)...
  • attributes_to_ignore_when_comparing слишком многословно.Не то чтобы это нужно было явно добавлять в каждую модель, если они хотят использовать значения по умолчанию для драгоценного камня.Возможно, допустим переопределение значения по умолчанию с помощью макроса класса, например ignore_for_attributes_eql :last_signed_in_at, :updated_at

Комментарии приветствуются ...

Обновление : вместо разветвления active_record_attributes_equality, Я написал совершенно новый камень, active_record_ignored_attributes , доступный по http://github.com/TylerRick/active_record_ignored_attributes и http://rubygems.org/gems/active_record_ignored_attributes

2 голосов
/ 18 февраля 2019

Я создал инструмент сравнения в 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

2 голосов
/ 24 июня 2015
 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
...