web-dev-qa-db-fra.com

Comment tester l'égalité d'objet (ActiveRecord)

Dans Ruby 1.9.2 sur Rails 3.0.3, J'essaie de tester l'égalité des objets entre deux Friend (la classe hérite de ActiveRecord::Base) objets.

Les objets sont égaux, mais le test échoue:

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

Juste pour les sourires, je teste également l'identité de l'objet, qui échoue comme je m'y attendais:

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.

Quelqu'un peut-il m'expliquer pourquoi le premier test d'égalité d'objet échoue et comment je peux affirmer avec succès que ces deux objets sont égaux?

49
chipotle_warrior

Rails délègue délibérément les contrôles d'égalité à la colonne d'identité. Si vous voulez savoir si deux objets AR contiennent les mêmes éléments, comparez le résultat de l'appel de #attributes sur les deux.

44
noodl

Jetez un œil aux documentation API sur le == (alias eql?) opération pour ActiveRecord::Base

Renvoie vrai si comparaison_objet est le même objet exact, ou comparaison_objet est du même type et self a un ID et il est égal à comparaison_objet.id.

Notez que les nouveaux enregistrements sont différents de tout autre enregistrement par définition , sauf si l'autre enregistrement est le récepteur lui-même. De plus, si vous récupérez des enregistrements existants avec select et laissez l'ID en dehors, vous êtes seul, ce prédicat retournera faux.

Notez également que la destruction d'un enregistrement préserve son ID dans l'instance de modèle, de sorte que les modèles supprimés sont toujours comparables.

40
Andy Lindeman

Si vous souhaitez comparer deux instances de modèle en fonction de leurs attributs, vous souhaiterez probablement exclure certains attributs non pertinents de votre comparaison, tels que: id, created_at, et updated_at. (Je considérerais que celles-ci sont plus des métadonnées sur l'enregistrement qu'une partie des données de l'enregistrement lui-même.)

Cela peut ne pas avoir d'importance lorsque vous comparez deux nouveaux enregistrements (non enregistrés) (puisque id, created_at, et updated_at sera tous nil jusqu'à ce qu'ils soient enregistrés), mais je trouve parfois nécessaire de comparer un objet enregistré avec un non enregistré un (dans auquel cas == vous donnerait faux puisque nul! = 5). Ou je veux comparer deux sauvegardés objets pour savoir s'ils contiennent les mêmes données (donc l'ActiveRecord == L'opérateur ne fonctionne pas, car il renvoie false s'ils ont des id différents, même s'ils sont par ailleurs identiques).

Ma solution à ce problème consiste à ajouter quelque chose comme ceci dans les modèles que vous souhaitez comparer en utilisant des attributs:

  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

Ensuite, dans mes spécifications, je peux écrire des choses aussi lisibles et succinctes que ceci:

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

Je prévois de bifurquer le active_record_attributes_equality gem et le changer pour utiliser ce comportement afin qu'il puisse être plus facilement réutilisé.

J'ai cependant quelques questions à vous poser:

  • Un tel bijou existe-t-il déjà ??
  • Comment appeler la méthode? Je ne pense pas que remplacer le == L'opérateur est une bonne idée, donc pour l'instant je l'appelle identical?. Mais peut-être quelque chose comme practically_identical? ou attributes_eql? serait plus précis, car il ne vérifie pas s'ils sont strictement identiques ( certains des attributs peuvent être différents.) ...
  • attributes_to_ignore_when_comparing est trop verbeux. Non pas que cela devra être explicitement ajouté à chaque modèle s'ils veulent utiliser les valeurs par défaut de la gemme. Peut-être permettre à la valeur par défaut d'être remplacée par une macro de classe comme ignore_for_attributes_eql :last_signed_in_at, :updated_at

Les commentaires sont les bienvenus ...

Mise à jour : au lieu de bifurquer le active_record_attributes_equality, J'ai écrit un tout nouveau bijou, active_record_ignored_attributes , disponible sur http://github.com/TylerRick/active_record_ignored_attributes = et http://rubygems.org/gems/active_record_ignored_attributes

20
Tyler Rick
 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
2
scott

J'ai créé un matcher sur RSpec juste pour ce type de comparaison, très simple, mais efficace.

Dans ce fichier: spec/support/matchers.rb

Vous pouvez implémenter ce 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

Après cela, vous pouvez l'utiliser lors de l'écriture d'une spécification, de la manière suivante ...

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

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

Liens utiles:

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

2
Victor