Using factory_girl in Rails with associations that have unique constraints. Getting duplicate errors
Just FYI, you can also use the initialize_with
macro inside your factory and check to see if the object already exists, then don't create it over again. The solution using a lambda (its awesome, but!) is replicating logic already present in find_or_create_by. This also works for associations where the :league is being created through an associated factory.
FactoryGirl.define do factory :league, :aliases => [:euro_cup] do id 1 name "European Championship" rank 30 initialize_with { League.find_or_create_by_id(id)} endend
I encountered the same problem and added a lambda at the top of my factories file that implements a singleton pattern, which also regenerates the model if the db has been cleared since the last round of tests/specs:
saved_single_instances = {}#Find or create the model instancesingle_instances = lambda do |factory_key| begin saved_single_instances[factory_key].reload rescue NoMethodError, ActiveRecord::RecordNotFound #was never created (is nil) or was cleared from db saved_single_instances[factory_key] = Factory.create(factory_key) #recreate end return saved_single_instances[factory_key]end
Then, using your example factories, you can use a factory_girl lazy attribute to run the lambda
Factory.define :product do |p| p.product_type { single_instances[:add_product_type] } #...this block edited as per comment belowend
Voila!
EDIT:
See an even cleaner solution at the bottom of this answer.
ORIGINAL ANSWER:
This is my solution to creating FactoryGirl singleton associations:
FactoryGirl.define do factory :platform do name 'Foo' end factory :platform_version do name 'Bar' platform { if Platform.find(:first).blank? FactoryGirl.create(:platform) else Platform.find(:first) end } endend
You call it e.g. like:
And the following platform versions exists: | Name | | Master | | Slave | | Replica |
In this way all 3 platform versions will have same platform 'Foo', i.e. singleton.
If you wanna save a db query you can instead do:
platform { search = Platform.find(:first) if search.blank? FactoryGirl.create(:platform) else search end}
And you can consider to make the singleton association a trait:
factory :platform_version do name 'Bar' platform trait :singleton do platform { search = Platform.find(:first) if search.blank? FactoryGirl.create(:platform) else search end } end factory :singleton_platform_version, :traits => [:singleton]end
If you want to setup more than 1 platform, and have different sets of platform_versions, you can make different traits which are more specific, i.e.:
factory :platform_version do name 'Bar' platform trait :singleton do platform { search = Platform.find(:first) if search.blank? FactoryGirl.create(:platform) else search end } end trait :newfoo do platform { search = Platform.find_by_name('NewFoo') if search.blank? FactoryGirl.create(:platform, :name => 'NewFoo') else search end } end factory :singleton_platform_version, :traits => [:singleton] factory :newfoo_platform_version, :traits => [:newfoo]end
Hope this is useful to some out there.
EDIT:
After submitting my original solution above, I gave the code another look, and found an even cleaner way to do this: You do not define traits in the factories, instead you specify the association when you call the test step.
Make regular factories:
FactoryGirl.define do factory :platform do name 'Foo' end factory :platform_version do name 'Bar' platform endend
Now you call the test step with the association specified:
And the following platform versions exists: | Name | Platform | | Master | Name: NewFoo | | Slave | Name: NewFoo | | Replica | Name: NewFoo |
When doing it like this, the creation of platform 'NewFoo' is using 'find_or_create_by' functionality, so the first call creates the platform, the next 2 calls finds the already created platform.
In this way all 3 platform versions will have same platform 'NewFoo', and you can create as many sets of platform versions as you need.
I think this is a very clean solution, since you keep the factory clean, and you even make it visible to the reader of your test steps that those 3 platform versions all have the same platform.