In Rails (3.0) test code, I've cloned an object s开发者_如何学Co I can clobber it for validation testing without changing the original. If I have called assert(original.valid?) before cloning, then the clone passes the validates_presence_of test even after I have set member_id value to nil.
The two tests below illustrate this. In test one, the clone is created before the original ("contact") is validated. Clone correctly fails the validation when member_id is missing. Assertion C succeeds.
In test two, the clone is created after the original is validated. Even though clone.member_id is set to nil, it passes the validation. In other words, assertion 2C fails. The only difference between the tests is the order of the two lines:
cloned = contact.clone
assert(contact.valid?,"A")
What is going on here? Is this normal Ruby behavior re: cloning that I just don't understand?
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
You wil be surprised - it cannot work!
Ok the reason can be found in the Rails code. First validation will run the code:
# Validations module
# Returns the Errors object that holds all information about
# attribute error messages.
def errors
@errors ||= Errors.new(self)
end
As it is a first run then it will create new instance of Errors class. Simple, isn't it? But there is a gotcha - the parameter is self. In your case it is "contact" object.
Later then when you call this again on cloned object, the @errors instance will not be created again - as it is not null. And there it is! Instead of passing "cloned" self, the older self is used.
Later in the validation code there the Errors class runs the code that read the value from @base which is the self from the initialization. Can you see it? The values for test are read from original model not from the clone! So the validation on "cloned" object runs on values from the original.
Ok, so far for the "why not" and now a few words about "how to".
The solution is simple - just set @errors to nil after cloning and before validation. As it is quite private, the simple assignment doesn't work. But this works:
cloned.instance_eval do
@errors = nil
end
And some tip for interesting reading: http://yehudakatz.com/2010/01/10/activemodel-make-any-ruby-object-feel-like-activerecord/
It is quite comprehensive explanation how the validations in Rails 3 works.
精彩评论