Saturday, May 23, 2009

Creating 'has_many :through' factories with Factory Girl

Factory girl is a great tool for setting up your database for testing. However, I had some trouble when setting up an application that utilizes has_many, :through associations in the models. How do you set up the factories for the two parents and the join table that has its own attributes and still maintain the connection between all three tables? After a couple of days, I solicited the factory_girl list and Josh Clayton pointed me in the right direction, (thanks Josh!)

BTW, I borrowed this particular application from http://www.jumbabox.com/2008/06/ruby-on-rails-many-to-many-tutorial/ if you need more explanation on the models used in this example.

On to the solution:
Here are the models:


class Album < ActiveRecord::Base
has_many :features
has_many :artists, :through => :features
end

class Artist < ActiveRecord::Base
has_many :features
has_many :albums, :through => :features
end

class Feature < ActiveRecord::Base
belongs_to :album
belongs_to :artist
end


As you can see, the Artist and Album models are the parents and the Feature model is the join table. This produces the desired many-to-many association with 'has_many /(:artists|:albums)/, :through => :features'.

Here are the factories... There are two big concepts that should be mentioned that allow this association to be created properly. For one, the associations in the factories should be created from the join table back to the parents. This enables the parents' records to be created at the same time that the join table is created with the link between the three intact.

# revised factories
# created the associations from the join table only
Factory.define(:artist) do |artist|
artist.name "FooFighters"
end

Factory.define(:album) do |album|
album.name "FooTime"
album.genre "FooRock"
album.description "A good time"
end

# join table factory - :feature
Factory.define(:feature) do |feature|
feature.association :artist
feature.association :album
end
The second big concept is that wherever you create your factories (in my case, I'm using cucumber and create the factories in './features/support/env.rb') the parents factories are only @instantiated and returned, though not written to the database. This is done using the 'Factory.build(:factory_name)' method rather than the Factory(:factory_name) or Factory.create(:factory_name) methods. If you used create you would save multiple records to the database with no associations to the join table. When the join table factory is 'create'd, the associations that were defined in the join table factory to the parents create the associated parent's records for you.

Here is how I create the factories in ./features/support/env.rb (NOTE: these factories are created in a Before block that kills all data in the database tables because we want to reset the database as we aren't using transactional fixtures, then reconstructs them with the factories):

Before do
#killing the table data as we aren't using fixtures
Album.destroy_all
Artist.destroy_all
Feature.destroy_all
#recreating the data
# **** Here's the magic ****
# building the parent factories first, and saving them
# through the join table so that the
# the join table record is fully created in db.
album = Factory.build(:album)
artist = Factory.build(:artist)
# using create here as the join table is written to
# the db first with id's from album and artist factories.
# you could use 'feature = Factory(:feature)'
# though using 'create' method for clarity.
feature = Factory.create(:feature)
end
This produces the following output from 'RAILS_ENV=test script/console':
Loading test environment (Rails 2.3.2)
>> @feature = Feature.find(:all)
=> [#Feature id: 8, artist_id: 10, album_id: 8, number_of_songs: nil,
created_at: "2009-05-23 15:03:25", updated_at: "2009-05-23 15:03:25"]
>> @artist = Artist.find(:all)
=> [#Artist id: 10, name: "FooFighters", created_at: "2009-05-23 15:03:25",
updated_at: "2009-05-23 15:03:25"]
>> @album = Album.find(:all)
=> [#Album id: 8, genre: "FooRock", name: "FooTime", description: "A good time",
created_at: "2009-05-23 15:03:25", updated_at: "2009-05-23 15:03:25"]
>>
Great Success!

I hope that was clear... hit me up if you are still having trouble with this.












No comments:

Post a Comment