views:

23

answers:

1

In Rails (3.0) test code, I've cloned an object so 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
+2  A: 

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.

pawien
Thanks, I'm going to have to really think a while about that one but doubtless I'll learn something in the process. I'm too used to simple procedural languages and sometimes the subtleties of objects totally pass me by.
Mike Blyth