2011-01-19 23 views
43

En Ruby 1.9.2 en Rails 3.0.3, estoy intentando probar la igualdad de objetos entre dos objetos Friend (clase hereda de ActiveRecord::Base).Cómo probar la igualdad de objetos (ActiveRecord)

Los objetos son iguales, pero la prueba falla:

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

Sólo para las muecas, también pongo a prueba de la identidad del objeto, lo que falla como yo esperaría:

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. 

Puede alguien explicar para mí, ¿por qué falla la primera prueba de igualdad de objetos y cómo puedo afirmar con éxito que esos dos objetos son iguales?

Respuesta

36

Rails delega deliberadamente las comprobaciones de igualdad a la columna de identidad. Si desea saber si dos objetos AR contienen el mismo material, compare el resultado de llamar #attributes en ambos.

+3

Pero en este caso la columna de identidad es 'las negativas para ambos casos porque ni tiene abeja n guardado 'eql?()' verifica tanto el valor como el tipo de un atributo. 'nil.class == nil.class' es' true' y 'nil == nil' es' true', por lo que el primer ejemplo de OP debería haber pasado verdadero. Tu respuesta no explica por qué está volviendo falso. – Jazz

+2

No solo compara ids ciegamente, solo compara ids si los ids son significativos. Como se menciona en la respuesta de Andy Lindeman: "Los nuevos registros son diferentes de cualquier otro registro por definición". – Lambart

33

Tome un vistazo a la API docs sobre el funcionamiento == (alias eql?) para ActiveRecord::Base

Devuelve true si comparison_object es el mismo objeto exacto, o comparison_object es del mismo tipo y auto tiene un ID y se es igual a comparison_object.id.

Tenga en cuenta que los nuevos registros son diferentes de cualquier otro registro por definición, a menos que el otro registro sea el receptor. Además, si recuperas los registros existentes con Select y dejas el ID, estás solo, este predicado devolverá false.

Tenga en cuenta también que la destrucción de un registro conserva su ID en la instancia del modelo, por lo que los modelos eliminados siguen siendo comparables.

+1

Enlace API docs actualizado para Rails 3.2.8 http://www.rubydoc.info/docs/rails/3.2.8/frames Además, cabe destacar que 'eql?' Está anulado, pero no el alias 'igual?' que aún compara 'object_id' –

16

Si desea comparar dos instancias de modelo en base a sus atributos, es probable que desee excluir a ciertos atributos irrelevantes de su comparación, como por ejemplo: id, created_at y updated_at. (Me considero aquellos a ser más metadatos sobre el registro de parte de los datos del registro mismo.)

Esto no podría ser importante cuando se está comparando dos nuevos registros (guardar) (desde id, created_at y updated_at a todos sean nil hasta que se guarden), pero a veces me parece necesario comparar un objeto guardado con un no guardado uno (en cuyo caso == le daría falso desde cero! = 5). O quiero comparar dos objetos guardados para averiguar si contienen los mismos datos (por lo que el operador ActiveRecord == no funciona, porque devuelve falso si tienen id's diferentes, incluso si son idénticos)

Mi solución a este problema es añadir algo como esto en los modelos que usted quiere estar utilizando atributos comparables:

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 

Luego, en las especificaciones de mi Puedo escribir tales cosas legibles y concisas como esto:

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

Estoy planeando bifurcar la gema active_record_attributes_equality y cambiarla para utilizar este comportamiento, de modo que pueda reutilizarse más fácilmente.

Algunas preguntas que tengo, sin embargo, incluyen:

  • ¿Existe ya una joya ??
  • ¿Cómo debe llamarse el método? No creo que anular el operador existente == es una buena idea, así que por ahora lo llamo identical?. Pero tal vez algo como practically_identical?attributes_eql? o sería más exacto, ya que no está comprobando si son estrictamente idéntica ( algunos de los atributos están autorizados a ser diferente.) ...
  • attributes_to_ignore_when_comparing es demasiado prolijo. No es necesario agregar esto explícitamente a cada modelo si quieren usar los valores predeterminados de la joya. Tal vez permita que el defecto sea anulado con una macro clase como ignore_for_attributes_eql :last_signed_in_at, :updated_at

Los comentarios son bienvenidos ...

actualización: En lugar de bifurcar el active_record_attributes_equality, escribí una joya de nueva construcción, active_record_ignored_attributes , disponible en http://github.com/TylerRick/active_record_ignored_attributes y http://rubygems.org/gems/active_record_ignored_attributes

1
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 
Cuestiones relacionadas