How to avoid circular creation of associated models in factory_girl?

后端 未结 2 757
南旧
南旧 2021-02-14 00:10

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

相关标签:
2条回答
  • 2021-02-14 00:27

    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
    
    0 讨论(0)
  • 2021-02-14 00:32

    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@example.com"
        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!

    0 讨论(0)
提交回复
热议问题