2010-09-26 12 views
2

Dans le code de test Rails (3.0), j'ai cloné un objet afin que je puisse l'écraser pour des tests de validation sans changer l'original. Si j'ai appelé assert (original.valid?) avant le clonage, le clone passe le test validates_presence_of même après avoir défini la valeur de member_id sur nil.Rails3: Le clonage d'un objet déjà validé empêche l'invalidation du clone - est-ce étrange ou normal?

Les deux tests ci-dessous illustrent cela. Dans le premier test, le clone est créé avant que l'original ("contact") soit validé. Cloner échoue correctement la validation lorsque member_id est manquant. L'assertion C réussit.

Dans le test 2, le clone est créé après l'original est validé. Même si clone.member_id est défini sur nul, transmet la validation. En d'autres termes, l'assertion 2C échoue. La seule différence entre les essais est l'ordre des deux lignes:

cloned = contact.clone 
    assert(contact.valid?,"A") 

ce qui se passe ici? Est-ce que ce comportement normal de Ruby est: le clonage que je ne comprends pas?

test "clone problem 1" do 
    contact = Contact.new(:member_id => 1) 
    cloned = contact.clone 
    assert(contact.valid?,"A") 
    cloned.member_id = nil 
    assert(!cloned.valid?,"C") 
end 

test "clone problem 2" do 
    contact = Contact.new(:member_id => 1) 
    assert(contact.valid?,"2A") 
    cloned = contact.clone 
    cloned.member_id = nil 
    assert(!cloned.valid?,"2C") 
end 
+0

J'ai oublié de mentionner que, dans le test 2, j'ai vérifié pour être sûr que cloned.member_id était nul après l'affectation, en utilisant assert (! Cloned.member_id). Il est nul, mais passe toujours la validation. –

Répondre

3

Vous serez surpris - cela ne peut pas fonctionner!

Ok la raison peut être trouvée dans le code Rails. La première validation exécutera le code:

# Validations module 

# Returns the Errors object that holds all information about 
# attribute error messages. 
def errors 
    @errors ||= Errors.new(self) 
end 

Comme il s'agit d'une première exécution, il créera une nouvelle instance de la classe Errors. Simple, n'est-ce pas? Mais il y a un gotcha - le paramètre est self. Dans votre cas c'est un objet "contact". Plus tard, lorsque vous l'appelez à nouveau sur un objet cloné, l'instance @errors ne sera pas créée à nouveau, car elle n'est pas NULL. Et voilà! Au lieu de passer le soi "cloné", le soi plus ancien est utilisé.

Plus tard dans le code de validation, la classe Errors exécute le code qui lit la valeur de @base qui est self à partir de l'initialisation. Peux-tu le voir? Les valeurs de test sont lues à partir du modèle original et non du clone! La validation sur l'objet "cloné" s'exécute donc sur les valeurs de l'original.

Ok, jusqu'ici pour le "pourquoi pas" et maintenant quelques mots sur "comment".

La solution est simple - il suffit de mettre @errors à zéro après le clonage et avant la validation. Comme il est tout à fait privé, l'affectation simple ne fonctionne pas. Mais cela fonctionne:

cloned.instance_eval do 
    @errors = nil 
end 

Et une astuce pour une lecture intéressante: http://yehudakatz.com/2010/01/10/activemodel-make-any-ruby-object-feel-like-activerecord/

Il est tout à fait expliquer comment les complète dans Rails 3 validations de travaux.

+0

Merci, je vais devoir y réfléchir un moment, mais sans doute j'apprendrai quelque chose dans le processus. Je suis trop habitué à des langages procéduraux simples et parfois les subtilités des objets me dépassent totalement. –