32

I've been struggling with setting up a has_many/through relationship using Factory Girl.

I have the following models:

class Job < ActiveRecord::Base
  has_many :job_details, :dependent => :destroy
  has_many :details, :through => :job_details
end

class Detail < ActiveRecord::Base
  has_many :job_details, :dependent => :destroy
  has_many :jobs, :through => :job_details
end

class JobDetail < ActiveRecord::Base
  attr_accessible :job_id, :detail_id
  belongs_to :job
  belongs_to :detail
end

My Factory:

factory :job do
  association     :tenant
  title           { Faker::Company.catch_phrase }
  company         { Faker::Company.name }
  company_url     { Faker::Internet.domain_name }
  purchaser_email { Faker::Internet.email }
  description     { Faker::Lorem.paragraphs(3) }
  how_to_apply    { Faker::Lorem.sentence }
  location        "New York, NY"
end

factory :detail do
  association :detail_type <--another Factory not show here
  description "Full Time"
end

factory :job_detail do
  association :job
  association :detail
end

What I want is for my job factory to be created with a default Detail of "Full Time".

I've been trying to follow this, but have not had any luck: FactoryGirl Has Many through

I'm not sure how the after_create should be used to attach the Detail via JobDetail.

Boann
  • 48,794
  • 16
  • 117
  • 146
cman77
  • 1,753
  • 1
  • 22
  • 48

6 Answers6

36

Try something like this. You want to build a detail object and append it to the job's detail association. When you use after_create, the created job will be yielded to the block. So you can use FactoryGirl to create a detail object, and add it to that job's details directly.

factory :job do
  ...

  after_create do |job|
    job.details << FactoryGirl.create(:detail)
  end
end
Logan Serman
  • 29,447
  • 27
  • 102
  • 141
  • This worked great thank you. One question - adding the after_create works, but it responds with `DEPRECATION WARNING: You're trying to create an attribute `detail_id'. Writing arbitrary attributes on a model is deprecated. Please just use `attr_writer` etc.` any ideas? – cman77 Jan 21 '13 at 22:07
  • 14
    I know this is old, but in FactoryGirl you now use callbacks with the format `after(:create)` instead of `after_create` The rest of the answer should still work without error. – Arel Oct 09 '13 at 14:55
  • more info on `after(:create)` callbacks: http://robots.thoughtbot.com/get-your-callbacks-on-with-factory-girl-3-3 – Brian Dec 08 '13 at 03:58
  • 4
    OK, this works for create, because there are IDs for JobDetail to reference, but what about `after(:build)`? Any way to have associations for unsaved objects? Or should I abandon the idea of things working that way? – Epigene Jul 22 '15 at 09:31
4

I faced this issue today and I found a solution. Hope this helps someone.

FactoryGirl.define do
  factory :job do

    transient do
      details_count 5 # if details count is not given while creating job, 5 is taken as default count
    end

    factory :job_with_details do
      after(:create) do |job, evaluator|
        (0...evaluator.details_count).each do |i|
          job.details << FactoryGirl.create(:detail)
        end
      end
    end
  end  
end

This allows to create a job like this

create(:job_with_details) #job created with 5 detail objects
create(:job_with_details, details_count: 3) # job created with 3 detail objects
Sasi_Kala
  • 231
  • 2
  • 4
  • works great with latest everything as of now (rails 5, rspec 3.5, factorygirl 4.8) – jpw Feb 12 '17 at 23:09
2

This worked for me

FactoryGirl.define do
  factory :job do

    # ... Do whatever with the job attributes here

    factory :job_with_detail do

      # In later (as of this writing, unreleased) versions of FactoryGirl
      # you will need to use `transitive` instead of `ignore` here
      ignore do
        detail { create :detail }
      end

      after :create do |job, evaluator|
        job.details << evaluator.detail
        job.save
        job_detail = job.job_details.where(detail:evaluator.detail).first

        # ... do anything with the JobDetail here

        job_detail.save
      end
    end
  end
end

Then later

# A Detail object is created automatically and associated with the new Job.
FactoryGirl.create :job_with_detail

# To supply a detail object to be associated with the new Job.
FactoryGirl.create :job_with_detail detail:@detail
Nate
  • 12,963
  • 4
  • 59
  • 80
  • I know this is old but I am curious as to what makes this better than the accepted answer here? – rorykoehler Aug 05 '15 at 15:45
  • When I read the original answer there wasn't enough information in the example for my liking. This also adds additional functionality on top of that answer since in you can use `create :job_with_detail` with or without the `detail:@detail` option and if not provided then the detail will be created automatically. – Nate Aug 05 '15 at 17:24
1

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 :job do
  job_details { [association(:job_detail)] }
end

FactoryBot.define :detail do
  description "Full Time"
end

FactoryBot.define :job_detail do
  association :job
  association :detail
end
thisismydesign
  • 21,553
  • 9
  • 123
  • 126
0

You can solve this problem in the following way:

FactoryBot.define do
  factory :job do
    # job attributes

    factory :job_with_details do
      transient do
        details_count 10 # default number
      end

      after(:create) do |job, evaluator|
        create_list(:details, evaluator.details_count, job: job)
      end
    end
  end
end

With this, you can create a job_with_details, that has options to specify how many details you want. You can read this interesting article for more details.

Nesha Zoric
  • 6,218
  • 42
  • 34
0

With the current factory_bot(previously factory_girl) implementation, everything is taken care by the gem, you don't need to create and then push the records inside the jobs.details. All you need is this

factory :job do
  ...

  factory :job_with_details do
    transient do
      details_count { 5 }
    end

    after(:create) do |job, evaluator|
      create_list(:detail, evaluator.details_count, jobs: [job])
      job.reload
    end
  end  
end

Below code will produce 5 detail jobs

 create(:job_with_details)
script
  • 1,667
  • 1
  • 12
  • 24