Как проверить равенство объектов (ActiveRecord)

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

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


person ryonlife    schedule 19.01.2011    source источник


Ответы (5)


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

person noodl    schedule 19.01.2011
comment
Но в этом случае столбец идентификаторов равен nil для обоих экземпляров, потому что ни один из них не был сохранен. eql?() проверяет как значение, так и тип атрибута. nil.class == nil.class равно true, а nil == nil равно true, поэтому первый пример OP все равно должен был пройти true. Ваш ответ не объясняет, почему он возвращает false. - person Jazz; 16.10.2012
comment
Он не просто слепо сравнивает идентификаторы, он сравнивает только идентификаторы, если они значимы. Как упоминалось в ответе Энди Линдемана: новые записи по определению отличаются от любых других записей. - person Lambart; 10.10.2013

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

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

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

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

person Andy Lindeman    schedule 19.01.2011
comment
Обновленная ссылка на документы API для Rails 3.2.8 rubydoc.info/docs/rails/3.2 .8 / frames Также следует отметить, что eql? переопределяется, но не псевдоним equal?, который по-прежнему сравнивает object_id - person Ben Simpson; 26.08.2013
comment
Это действительно лучший ответ, чем текущий правильный ответ. Документы для == объясняли суть всего, что мне нужно было знать, чтобы выяснить, как rails проверяет равенство объектов. - person kapad; 09.09.2018

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

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

person Tyler Rick    schedule 17.08.2011

Я создал сопоставление на 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/gotitbot/factory_bot

person Victor    schedule 18.02.2019

Если вы, как и я, ищете ответ Minitest на этот вопрос, то вот специальный метод, который утверждает, что атрибуты двух объектов равны.

Предполагается, что вы всегда хотите исключить атрибуты id, created_at и updated_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
person Tom Luce    schedule 02.10.2020