How to set up factory in FactoryBot with has_many association
Asked Answered
H

7

103

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!

Hatband answered 5/8, 2011 at 22:30 Comment(0)
W
59

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
Wispy answered 11/10, 2011 at 23:4 Comment(1)
did you end up using association :features, [:factory => :feature]?Module
W
116

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
Womanizer answered 25/9, 2013 at 17:11 Comment(3)
This is the cat's meow. The ability to both build and create makes it the most versatile pattern. Then use this custom FG build strategy gist.github.com/Bartuz/74ee5834a36803d712b7 to post nested_attributes_for when testing controller actions that accepts_nested_attributes_forGatewood
upvoted, far more readable and versatile than the accepted answer IMOHeinie
As of FactoryBot 5, the association keyword uses the same build strategy for parent and child. So, it can build objects w/o saving to the database.Belligerent
W
59

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
Wispy answered 11/10, 2011 at 23:4 Comment(1)
did you end up using association :features, [:factory => :feature]?Module
N
42

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

Nucleolus answered 23/1, 2017 at 18:39 Comment(0)
C
20

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
Crosscrosslet answered 15/11, 2013 at 3:51 Comment(0)
F
6

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
Floodgate answered 14/12, 2021 at 20:45 Comment(1)
As suggested by @sequielo below, I needed to use [association(:post, user: instance)] to avoid creating an additional post.Lucey
P
2

Similar to @thisismydesign, however it created an additional post on my end (FactoryBot v6.2).

To avoid this situation, I've added keyword instance as below:

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, user: instance)] }
    end
  end
end
Polyphone answered 24/3, 2022 at 18:59 Comment(1)
This solved it for me too. But what is "instance" and why is it needed?Lucey
C
0

Here is how I set mine up:

# Model 1 PreferenceSet
class PreferenceSet < ActiveRecord::Base
  belongs_to :user
  has_many :preferences, dependent: :destroy
end

#Model 2 Preference

class Preference < ActiveRecord::Base    
  belongs_to :preference_set
end



# factories/preference_set.rb

FactoryGirl.define do
  factory :preference_set do
    user factory: :user
    filter_name "market, filter_structure"

    factory :preference_set_with_preferences do
      after(:create) do |preference|
        create(:preference, preference_set: preference)
        create(:filter_structure_preference, preference_set: preference)
      end
    end
  end

end

# factories/preference.rb

FactoryGirl.define do
  factory :preference do |p|
    filter_name "market"
    filter_value "12"
  end

  factory :filter_structure_preference, parent: :preference do
    filter_name "structure"
    filter_value "7"
  end
end

And then in your tests you can do:

@preference_set = FactoryGirl.create(:preference_set_with_preferences)

Hope that helps.

Concha answered 8/9, 2016 at 16:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.