Factory girl, dependent factories
Asked Answered
S

1

5

UPDATE

I went back to using Fixtures. IMOP, fixtures are FAR better than factories; easier to use, easier to write, easier to understand (no magic). My suggestion: limit your testing library to the very basics (listen to DHH)...use minitest with fixtures.

original post

In my app a district has many schools, a school has many uses, a user has many accounts, an account has one role. In order to create complete factories for testing I need to create a user and school that persists across factories. Im getting a "stack level too deep" error in my recent attempts.

My user_test.rb

 FactoryGirl.define do

   factory :district do
     name "Seattle"
   end

   factory :school do
     association :primarycontact, factory: :user # expecting this to attach the user_id from factory :user as :primary contact_id in the school model
     association :district, factory: :district # expecting this to attach the :district_id from the :district factory as :district_id in the school model
     name "Test School"
   end

   factory :user do, aliases: [:primarycontact]
     email "[email protected]"
     name "Who What"
     username "wwhat"
     password "123456"
     password_confirmation { |u| u.password }
     association :school, factory: :school # expecting this to create :school_id in the users model, using the :school factory
   end

   factory :role do
     name "student"
   end

   factory :account do
     association :user, factory: :user
     association :role, factory: :role
   end

 end

So, I am attempting to do FactoryGirl.create(:account)... which I am expecting to create an account, with the user and role from the factories above, with the user associated with the school that is associated with the district. This is not working for me. Among failing tests I get a "stack level too deep" error. And, I believe my before each DatabaseCleaner.clean is clearing the test db before each new factory.

The test that calls these factories is:

describe "User integration" do

  def log_em_in
    visit login_path
    fill_in('Username', :with => "wwhat")
    fill_in('Password', :with => "123456")
    click_button('Log In')
  end

  it "tests log in" do
    user = FactoryGirl.create(:account)
    log_em_in
    current_path.should == new_user_path
  end

end

.

current_path.should == new_user_path returns unknown method error 'should'

How can I improve this code to nest the factories correctly and get a current_user in order to continue testing?

MODELS

school.rb

  belongs_to :district
  belongs_to :primarycontact, :class_name => "User"
  has_many :users, :dependent => :destroy

user.rb

  belongs_to :school
  has_many :accounts, :dependent => :destroy

district.rb

  has_many :schools

account.rb

  belongs_to :role
  belongs_to :user

role.rb

  has_many :accounts
  has_many :users, :through => :accounts
Selfcentered answered 30/12, 2012 at 3:8 Comment(8)
You write that a school has_many users, but in your factory it looks like it has just one user, as primarycontact. Can you post the code for these models (just the first lines with the association info)?Marquess
p.s. just glancing at your factory code, it seems pretty clear that the problem is that you have a circular dependency between your school factory and your user factory: a school has a primarycontact, which is created using the user factory, but user also has a school which is defined using the school factory.Marquess
School belongs_to :primarycontact, :class_name => "User" and has_many :users. Not sure why I did it that way (old code)...I think I'll change it to define the primary contact using a boolean in the user model. Ill add my existing model code above...thanksSelfcentered
You seem to be correct about my circular dependency. I removed the validation for primarycontact from school model, and removed primaycontact from my factories...and the test past and user logged in.Selfcentered
I set up my school model to has_many users, but also to belong_to :primarycontact, :class => "User" so that only one user could ever be the primary. I could use a boolean and some validations in the user model instead to achieve this...I guess I thought this way was easier when I set it up initially. Is there a problem (other than what I experienced in testing) with my existing approach?Selfcentered
shioyama...create a quick answer so I can accept it.Selfcentered
I'll write something up in a day or so, no time right now.Marquess
Ok posted an answer, please have a look!Marquess
M
12

Your basic problem is that you have a circular dependency between your user factory and your school factory, caused by the fact that you create a primarycontact (a user) when you create a school, then that user creates a school, and so on.

You can get around this by changing how you define your school association inside the user factory. Before doing that though, I'd suggest as a general rule using the shorthand notation for associations. So replace this:

factory :account do
  association :user, factory: :user
  association :role, factory: :role
end

with this:

factory :account do
  user
  role
end

Using this simplification, the following factories will do what you want without generating any circular dependency:

FactoryGirl.define do

  factory :district do
    name "Seattle"
  end

  factory :school do |school|
    district
    primarycontact
    name "Test School"
    after_build do |s|
      s.primarycontact.school = s
    end
  end

  factory :user do
    email "[email protected]"
    name "Who What"
    username "wwhat"
    password "123456"
    password_confirmation { |u| u.password }
    school
  end

  factory :primarycontact, class: "User" do
    # add any attributes you want the primarycontact user to have here
  end

  factory :role do
    name "student"
  end

  factory :account do
    user
    role
  end

end

Notice that what I have done is to create a factory for primarycontact with the class: "User" option. Unlike the user factory, this factory does not create the school by default, avoiding the circular dependency.

Then in the school factory, I use an after_build callback to assign the school itself to the school association on primarycontact, rather than creating a new school (which was causing the problem in your factories).

Hope that makes sense. Note that the callback syntax has changed in the more recent version of factory_girl, see the documentation for details.

Marquess answered 1/1, 2013 at 8:14 Comment(2)
Does this mean I need to re-define everything in :primarycontact that I define in :user ? (email "[email protected]" etc)Cynthia
I recently gave up on Factories and went back to fixtures. I love it! I will never user factories again.Selfcentered

© 2022 - 2024 — McMap. All rights reserved.