How to avoid circular creation of associated models in factory_girl?
Asked Answered
S

2

14

I have an app where a user can sign in with multiple services, e.g. Google Plus, Facebook, Twitter, etc.

To facilitate this, I have a base User model which has_many Identity records.

  • Each Identity record has a provider field (e.g. "Google", "Facebook", etc...) to indicate what provider is used to sign in.
  • There's an ActiveRecord validation that only lets a user have one of each type of provider. So a user can't have 2 "Google" identities.

I set up my factories as follows:

FactoryGirl.define do
  factory :user do
    sequence(:name) { |n| "Julio Jones-#{n}"}
    sequence(:email) { |n| "julio.jones-#{n}@atl.com" }

    after(:create) do |user|
      create(:identity, user: user)
    end
  end

  factory :identity do
    user

    provider "Google"
    email { user.email }
    password "password"
  end
end

The User model has a callback that creates an Identity record. It works great when running

user = FactoryGirl.create(:user)

However, if I create the identity instead

identity = FactoryGirl.create(:identity)

the identity factory will first try to create a parent user, which will in turn create another identity. When it finally gets back to creating the identity I made the call to, another identity already exists with the same provider for that user and it fails.

Essentially, I need a way for the after(:create) callback to NOT trigger when the user is being created by the :identity factory. Is there a way to tell what made the call to create a particular factory?

Stigmatic answered 9/4, 2016 at 18:42 Comment(0)
U
6

I don't think there's a nice way for a factory to tell that it's been called by another without collaboration. (You can always inspect caller_locations, but that's not nice.) Instead, have one factory tell the other to behave differently using a transient attribute:

FactoryGirl.define do
  factory :user do
    transient do
      create_identity true
    end

    after(:create) do |user, evaluator|
      if evaluator.create_identity
        create(:identity, user: user)
      end
    end

  end

  factory :identity do
    association :user, factory: :user, create_identity: false
  end

end
Utopia answered 10/4, 2016 at 15:37 Comment(1)
Thanks! This is probably the most straightforward and robust solutionStigmatic
A
17

Using transient attributes, as pointed out by Dave, is one option. Another option is to pass nil when building the associated factory.

FactoryGirl: Avoiding circular/infinite loops between associations

Let me illustrate with an example:

FactoryGirl.define do
  factory :user do
    sequence(:name) { |n| "Julio Jones-#{n}"}
    sequence(:email) { |n| "julio.jones-#{n}@atl.com" }
    # we pass user: nil here because it will cause the identity factory
    # to just skip the line user { ... }.
    identity { build(:identity, user: nil) }
  end

  factory :identity do
    # we pass user: nil here because it will cause the user factory
    # to just skip the line idenitity { ... }.
    user { build(:user, identity: nil) }
    provider "Google"
    email "[email protected]"
    password "password"
  end
end 

When we call build(:user), the code eventually reaches the following line:

identity { build(:identity, user: nil) }

This calls the identity factory. When it reaches the line that would normally build the user association (user { build(:user, identity: nil) }), it skips it because user has already been set (to nil). Congratulations, you just avoided the circular dependency!

It works the same way when you call build(:identity).


FactoryGirl: Accessing attributes from one factory in the associated factory

There's one last thing: In your case, you need to access the email attribute of the user in your identity factory. In your code example, you say:

factory :identity do
  ...
  email { user.email }
end

Obviously, this fails when we call build(:user) since we set user to nil when we call the identity factory. Fear not! We simply pass a new user object with the email when we call the identity factory. So the line becomes:

identity { build(:identity, user: User.new(email: email)) }

This will both prevent the circular, infinite association loop as well as make sure that the email attribute is available in the identity factory.

So finally, your code would look like this:

FactoryGirl.define do
  factory :user do
    sequence(:name) { |n| "Julio Jones-#{n}"}
    sequence(:email) { |n| "julio.jones-#{n}@atl.com" }
    # we pass user: User.new here because it will...
    # a) cause the identity factory to skip the line user { ... } and
    # b) allow us to use the email attribute in the identity factory.
    identity { build(:identity, user: User.new(email: email)) }
  end

  factory :identity do
    # we pass user: nil here because it will cause the user factory
    # to just skip the line idenitity { ... }.
    user { build(:user, identity: nil) }
    provider "Google"
    email { user.email }
    password "password"
  end
end 

Hope it's helpful!

Acromegaly answered 4/8, 2017 at 6:51 Comment(0)
U
6

I don't think there's a nice way for a factory to tell that it's been called by another without collaboration. (You can always inspect caller_locations, but that's not nice.) Instead, have one factory tell the other to behave differently using a transient attribute:

FactoryGirl.define do
  factory :user do
    transient do
      create_identity true
    end

    after(:create) do |user, evaluator|
      if evaluator.create_identity
        create(:identity, user: user)
      end
    end

  end

  factory :identity do
    association :user, factory: :user, create_identity: false
  end

end
Utopia answered 10/4, 2016 at 15:37 Comment(1)
Thanks! This is probably the most straightforward and robust solutionStigmatic

© 2022 - 2024 — McMap. All rights reserved.