Wednesday, May 9, 2007

New notation for standard_params

Andy Kotlinski came up with this new notation for standard_params. We found that over time, the use of standard_params became the default way to set up ActiveRecord objects in our test code, so Andy created a "new" and "create!" method, each of which take a class (or a symbol of the class name) as their first parameter. Since our standard_params methods are named using the convention of [class_name]_standard_params, we simply create an object of the passed-in class, passing in the hash returned by the appropriate standard_params method. We exclude any attributes that are passed in via the :without array, and merge any attributes that are passed in using the :merge hash.

Using a class name:

aeroplane = create! Aeroplane,
:without => [:capacity, :safety_rating],
:merge => {:model_number => "567890"}

Using a symbol of the class name:

aeroplane = create! :aeroplane,
:without => [:capacity, :safety_rating],
:merge => {:model_number => "567890"}

Both of these versions will construct an Aeroplane object using the hash of parameters returned by the conventionally named method:

def aeroplane_standard_params
{
:model_number => "123456",
:name => "A630",
:manufacturer => "Airbus",
:safety_rating => "5",
:capacity => "200"
}
end

removing any attributes specified in the :without Array, and merging any attributes specified in the
:merge Hash.

We also added a :quantity attribute, which causes an Array of objects to be returned containing the number of objects specified. For example, :quantity => 2 would cause two Aeroplane objects to be returned:
                   
plane1, plane2 = new :aeroplane, :quantity => 2

The "new" and "create!" methods look like this:

def new(klass, options={})
new_or_create(:new, klass, options)
end

def create!(sym, options={})
new_or_create(:create!, sym, options)
end

def new_or_create(operation, sym, options={})
raise ApplicationError.new("Option keys must be one of: [:without, :merge, :quantity]") /
unless options.without(:without, :merge, :quantity).empty?

klass, underscored = Class === sym ? [sym, sym.to_s.underscore] : [sym.to_s.camelize, sym]
src = <<-EOS
#{klass}.#{operation}(#{underscored}_standard_params.without(options[:without]).merge(options[:merge]||{}))
EOS

return eval(src) if options[:quantity].blank?
results = []
options[:quantity].times do
results << (eval src)
end
results
end


The notation also works well with nested objects. A pilot object can be merged in, using the pilot_standard_params for construction:

plane_with_pilot = new :aeroplane,
:merge => {
:model_number => "12345",
:pilot => new(:pilot)
}


One must be careful using Extract Method in tests. If you hide away important setup information or assertion code, then failing tests can sometimes be more difficult to fix. I make an exception in cases such as this, where unimportant information to the test is hidden away, thus making the important information more obvious to the reader.

No comments: