How to set up factory in FactoryGirl with has_many association
Can someone tell me if I'm just going about the setup the wrong way?
I have the following models that have has_many.through associations:
class Listing < ActiveRecord::Base
attr_accessible ...
has_many :listing_features
has_many :features, :through => :listing_features
validates_presence_of ...
...
end
class Feature < ActiveRecord::Base
attr_accessible ...
validates_presence_of ...
validates_uniqueness_of ...
has_many :listing_features
has_many :listings, :through => :listing_features
end
class ListingFeature < ActiveRecord::Base
attr_accessible :feature_id, :listing_id
belongs_to :feature
belongs_to :listing
end
I'm using Rails 3.1.rc4, FactoryGirl 2.0.2, factory_girl_rails 1.1.0, and rspec. Here is my basic rspec rspec sanity check for the :listing
factory:
it "creates a valid listing from factory" do
Factory(:listing).should be_valid
end
Here is Factory(:listing)
FactoryGirl.define do
factory :listing do
headline 'headline'
home_desc 'this is the home description'
association :user, :factory => :user
association :layout, :factory => :layout
association :features, :factory => :feature
end
end
The :listing_feature
and :feature
factories are similarly setup.
If the association :features
line is commented out, then all my tests pass.
When it is
association :features, :factory => :feature
the error message is
undefined method 'each' for #<Feature>
which I thought made sense to me because because listing.features
returns an array. So I changed it to
association :features, [:factory => :feature]
and the error I get now is ArgumentError: Not registered: features
Is it just not sensible to be generating factory objects this way, or what am I missing? Thanks very much for any and all input!
Solution 1:
Alternatively, you can use a block and skip the association
keyword. This makes it possible to build objects without saving to the database (otherwise, a has_many association will save your records to the db, even if you use the build
function instead of create
).
FactoryGirl.define do
factory :listing_with_features, :parent => :listing do |listing|
features { build_list :feature, 3 }
end
end
Solution 2:
Creating these kinds of associations requires using FactoryGirl's callbacks.
A perfect set of examples can be found here.
https://thoughtbot.com/blog/aint-no-calla-back-girl
To bring it home to your example.
Factory.define :listing_with_features, :parent => :listing do |listing|
listing.after_create { |l| Factory(:feature, :listing => l) }
#or some for loop to generate X features
end
Solution 3:
You could use trait
:
FactoryGirl.define do
factory :listing do
...
trait :with_features do
features { build_list :feature, 3 }
end
end
end
With callback
, if you need DB creation:
...
trait :with_features do
after(:create) do |listing|
create_list(:feature, 3, listing: listing)
end
end
Use in your specs like this:
let(:listing) { create(:listing, :with_features) }
This will remove duplication in your factories and be more reusable.
https://robots.thoughtbot.com/remove-duplication-with-factorygirls-traits
Solution 4:
I tried a few different approaches and this is the one that worked most reliably for me (adapted to your case)
FactoryGirl.define do
factory :user do
# some details
end
factory :layout do
# some details
end
factory :feature do
# some details
end
factory :listing do
headline 'headline'
home_desc 'this is the home description'
association :user, factory: :user
association :layout, factory: :layout
after(:create) do |liztng|
FactoryGirl.create_list(:feature, 1, listing: liztng)
end
end
end
Solution 5:
Since FactoryBot v5, associations preserve build strategy. Associations are the best way to solve this and the docs have good examples for it:
FactoryBot.define do
factory :post do
title { "Through the Looking Glass" }
user
end
factory :user do
name { "Taylor Kim" }
factory :user_with_posts do
posts { [association(:post)] }
end
end
end
Or with control over the count:
transient do
posts_count { 5 }
end
posts do
Array.new(posts_count) { association(:post) }
end