07 May 2009

Factory Girl for the win

In my last post I talked about testing and in this post I’d like to continue that theme.

As a classicist I like to create actual objects when I test. I find it makes my tests easier to write, shorter and more readable, but creating valid objects can be a messy business.

 1 class ProductControllerTest < ActionController::TestCase
 2   def test_should_show_price
 3     product = Product.create!(
 4                   :name => 'Dragons',
 5                   :brand => '5/10',
 6                   :sku => 'ABC',
 7                   :ean => '510DRAG',
 8                   :price => '75.00')
 9     
10     get :show, :sku => 'ABC'
11     assert_select '.price', '12.50'
12   end
13 end

In the test above we only care about the price and the sku of the product, having all these other attributes is messy but a valid Product requires all of them. It is necessary to create a valid object but we don’t need to do it in this clunky, verbose way. Enter the Object Mother

Essentially the Object Mother is just a factory for creating test objects that allows use to concentrate on the attributes we care about in the test.

 1 class Mother
 2   def self.create_product(attributes = {})
 3     Product.create!({
 4       :name => 'Product',
 5       :brand => 'Brand Name',
 6       :sku => 'SKU',
 7       :ean => 'EAN',
 8       :price => 10.00}.merge(attributes))
 9   end
10 end
11   
12 class ProductControllerTest < ActionController::TestCase
13   def test_should_show_price
14     product = Mother.create_product({:price => 75.00, :sku => 'ABC'})
15     
16     get :show, :sku => 'ABC'
17     assert_select '.price', '75.00'
18   end
19 end

Mission accomplished, the test is much more succinct because the data we created in the test is relevant to that test. But that’s not the end of the story. The example above is very simple, what if in our test we wanted 2 or more products and a valid product has unique EANs and SKUs.

Option 1) Pass the unique parts into the Mother – but that’ll pollute the test with irrelevant data again.
Option 2) Modify the Mother to dynamically create values for all the unique attributes
Option 3) Use something that already does Option 2 and a whole load of other things

Enter Factory Girl

 1 #tests/factories.rb
 2 Factory.sequence :sku do |n|
 3   "SKU#{n}"
 4 end
 5 Factory.sequence :ean do |n|
 6   "EAN#{n}"
 7 end
 8 Factory.define :product do |p|
 9   p.name "Product"
10   p.brand "Brand"
11   p.price 12.50
12   p.sku{ Factory.next :sku } #dynamically set the sku
13   p.ean{ Factory.next :ean } #dynamically set the ean
14 end
15 
16 #controller test
17 class ProductControllerTest < ActionController::TestCase
18   def test_should_show_price
19     product = Factory(:product, :price => 75.00, :sku => 'ABC')
20     
21     get :show, :sku => 'ABC'
22     assert_select '.price', '75.00'
23   end
24 end

Not only does Factory Girl do the job of make the test readable and relevant, it also handles sequences (like the ean and sku values), associates and more.