Neil
Neil

Reputation: 5178

FactoryGirl and RSpec: create a record with required nested_attributes for specs

I have a model contact that has_many :locations, through: :relationships, as well as has_many :teams, through: :contacts_teams.

A contact must have an associated team and location in order to pass validations. In other words: a new contact must have an associated relationship record and an associated contacts_team record. Below are the models:

#models/contact.rb
class Contact < ActiveRecord::Base
  has_many :contacts_teams
  has_many :teams, through: :contacts

  has_many :relationships, dependent: :destroy
  has_many :locations, through: :relationships

  accepts_nested_attributes_for :contacts_teams, allow_destroy: true

  # upon create, validate that at least one associated team and one associated location exist
  validate :at_least_one_contacts_team
  validate :at_least_one_relationship

  private

  def at_least_one_contacts_team
    return errors.add :base, "Must have at least one Team" unless contacts_teams.length > 0
  end

  def at_least_one_relationship
    return errors.add :base, "Must have at least one Location" unless relationships.length > 0
  end
end

#models/contacts_team.rb
class ContactsTeam < ActiveRecord::Base
  belongs_to :contact
  belongs_to :team
end

#models/team.rb
class Team < ActiveRecord::Base
  has_many :contacts_teams
  has_many :contacts, through: :contacts_teams
end

#models/relationship.rb
class Relationship < ActiveRecord::Base
  belongs_to :contact
  belongs_to :location
end

#models/location.rb
class Location < ActiveRecord::Base
  has_many :relationships
  has_many :contacts, through: :relationships
end

For testing: with factory_girl I want to create a contact factory that is able to successfully create a contact record. Since each contact record requires an associated contacts_team record and relationship record: when I create the contact record is should create those as well. Likewise: the contacts_team record should have an existing team it is associated to, and the relation record should have an existing location it is associated to. So essentially it should create a location and a team record as well.

How can I create a contact record with a factory, which in effect creates an associated contacts_team record and a relationship record?

Here are my current factories:

FactoryGirl.define do
  factory :contact do
    first_name "Homer"
    last_name "Simpson"
    title "Nuclear Saftey Inspector"
  end
end

FactoryGirl.define do
  factory :contacts_team do
  end
end

FactoryGirl.define do
  factory :team do
    name "Safety Inspection Team"
  end
end

FactoryGirl.define do
  factory :relationship do
  end
end

FactoryGirl.define do
  factory :location do
    name "Safety Location"
  end
end

If it is difficult/not possible to do this with factory_girl: how can I do it with straight rspec? The issue is that I can't create a contacts_team record or a relationship record, because the contact it is associated to doesn't exist yet! And I can't create a contact record because an associated contacts_team record or a relationship record doesn't exist yet. It seems like I'm trapped, but there has to be a way to do this that is not sloppy.

Upvotes: 2

Views: 2633

Answers (3)

Neil
Neil

Reputation: 5178

Here is the answer. We need to build the associated records (the contacts_team record and the relationship record) and then we save all records at the exact same time to the database (just like how nested attributes get saved by rails):

#factories/contact.rb
FactoryGirl.define do
  factory :contact do
    first_name "Homer"
    last_name "Simpson"
    title "Nuclear Saftey Inspector"
    agency
    contacts_teams {build_list :contacts_team, 1 }
    relationships {build_list :relationship, 1 }
  end
end

#factories/contacts_teams.rb
FactoryGirl.define do
  factory :contacts_team do
    team
  end
end

#factories/teams.rb
FactoryGirl.define do
  factory :team do
    name "Safety Inspection Team"
  end
end

#factories/relationships.rb
FactoryGirl.define do
  factory :relationship do
    location
  end
end

#factories/locations.rb  
FactoryGirl.define do
  factory :location do
    name "Safety Location"
  end
end

Then all you need to do is this:

create(:contact)

And with that it all at once creates a contact record, a team record, a location record, the associated contacts_team record, and the associated relationship record.

Upvotes: 1

Noam Hacker
Noam Hacker

Reputation: 4825

I just had a similar requirement last week.

At the end of your factory, you can call the next factory, and they will then be related. For example:

/spec/factories/contacts.rb

FactoryGirl.define do

    factory :contact do |c|
        first_name "Homer"
        last_name "Simpson"
        title "Nuclear Saftey Inspector"

        # now, call the other two factories
        relationship
        contacts_team
    end


    factory :contacts_team do
        # call the team factory
        team
    end


    factory :relationship do
        # call the location factory
        location
    end


    # define the team and location factories...

end

Now, in /spec/controllers/contacts_controller_spec.rb

contact = FactoryGirl.create(:contact)

You can just use factory girl to create a contact, even if you just need, for example, a location, because everything will be generated at once.

ALTERNATIVE (rspec)

don't "chain" your factories, instead in /spec/controllers/contacts_controller_spec.rb

contact = FactoryGirl.create(:contact)
# use .create_list(model, number, parent) to make children of a specific parent
contacts_team = FactoryGirl.create_list(:contacts_team, 3, :contact => contact)
relationship = FactoryGirl.create_list(:relationship, 3, :contact => contact)
team = FactoryGirl.create_list(:team, 3, :contacts_team => contacts_team)
location = FactoryGirl.create_list(:location, 3, :relationship => relationship)

This will create a contact, with 3 contact_teams (with 3 teams), and with 3 relationships (with 3 locations)

Hope this helps you figure out the correct pattern to make your test data :)

Upvotes: 1

oreoluwa
oreoluwa

Reputation: 5633

Basically, you can do it multiple ways. But one way that's often been advised is to use the FactoryGirl after(:create) or after(:build). In your case if you have the factory for all your models, you can easily call:

after(:create) do |team, evaluator| create_list(:contact_team, 5, team: team) end Based on the documentation, after(:create) yields the team and the evaluator, which stores all values from the factory, including transient attributes

This would create 5 contact_teams associated with the team for you. For each factory if you define all the relationships like this, you would have your full associations defined. The alternative would be to follow Noam's suggestion, which is also good, but has its own limits. You can find more information here: FactoryGirl Associations

Upvotes: 0

Related Questions