For each of our ActiveRecord model objects, we have a private test method within the test class called "standard_params". It returns a Hash containing parameters which, if passed to the constructor of the ActiveRecord object, will result in the construction of a valid object.
The benefits of this method are two-fold:
1. It improves the readability of tests. If the ActiveRecord object has a long list of attributes, and you are testing behaviour which is dependent only on one or two of these attributes, you do not have to explicitly specify all of the parameters - you simply merge in the ones that you really care about.
For example:
Let's say an Aeroplane object is invalid unless it has a 6 digit :model_number. It also has a list of required attributes, such as :name, :manufacturer, :safety_rating and :capacity. It would be invalid without any of these attributes.
Without the standard_params method, our test for invalidity without a six-digit model number would look like this:
With standard_params it would change to:
def test_invalid_without_six_digit_model_number
aeroplane = Aeroplane.new(:model_number => "12345", :name => "A630", :manufacturer => "Airbus", :safety_rating => "5", :capacity => "200")
assert_equal false, aeroplane.valid?
assert_equal false, aeroplane.errors.invalid?(:model_number)
end
:model_number is the only attribute explicitly specified in the test, thus removing a lot of "noise" in the test setup data - something which aids readability.
def test_invalid_without_six_digit_model_number
aeroplane = Aeroplane.new(standard_params.merge(:model_number => "12345"))
assert_equal false, aeroplane.valid?
assert_equal false, aeroplane.errors.invalid?(:model_number)
end
private
def standard_params
{
:model_number => "123456",
:name => "A630",
:manufacturer => "Airbus",
:safety_rating => "5",
:capacity => "200")
}
end
The second benefit is:
2. Tests are DRY-er (in a good way). I'm a firm believer that tests do not necessarily need to be DRY - readability is far more important. "Extract method" in tests can often be your worst enemy if done in a naive way. I think it is acceptable in this case because the part of the setup data that is extracted is not important to the test - that which *is* important is merged explicitly into the Hash in the test method itself. And so the usual benefits of DRY-ness result - when another attribute is added to the model object which itself has associated validations, the test methods that require construction of valid objects do not have to change - only the standard_params method needs to change.
We also added a "without" method to Hash, which removes elements specified in the parameters:
module HashExtension
def without(*keys)
clone = self.dup
keys.each { |key| clone.delete(key) }
clone
end
end
Hash.send :include, HashExtension
We use this in combination with standard_params to test for validates_presence_of clauses. Eg
def test_invalid_without_name
aeroplane = Aeroplane.new(standard_params.without(:name))
assert_equal false, aeroplane.valid?
assert_equal false, aeroplane.errors.invalid?(:name)
end

4 comments:
It's often baffled me that there was no compliment to merge()/merge!()--that I could find--in the standard libraries.
I'd thought of except() but without() is nice.
This appears to be a derivative of the 'Object Mother pattern' (I think it's a bad name, I prefer "object creator")
http://www.thoughtworks.com/object-mother-easing-test-object-creation.pdf.
The Ruby syntax makes the code nice and concise, though.
Nice job Shane - Joe Poon and I just used it.
We miss you!
This is a good idea, we have been doing it with all of our RSpec suites since we saw the Luke RedPath article http://www.lukeredpath.co.uk/2006/8/29/developing-a-rails-model-using-bdd-and-rspec-part-1
It really does clean things up.
Post a Comment