如何避免在factory_girl中循环创建关联模型?

use*_*003 12 ruby rspec ruby-on-rails factory-bot

我有一个应用程序,用户可以使用多种服务登录,例如Google Plus,Facebook,Twitter等.

为了方便这一点,我有一个记录的基础User模型has_many Identity.

  • 每个Identity记录都有一个provider字段(例如"Google","Facebook"等等),以指示用于登录的提供程序.
  • 有一个ActiveRecord验证,只允许用户拥有每种类型的提供者.所以用户不能拥有2 "Google" identities.

我按如下方式设置工厂:

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
Run Code Online (Sandbox Code Playgroud)

User模型具有创建Identity记录的回调.它运行时效果很好

user = FactoryGirl.create(:user)
Run Code Online (Sandbox Code Playgroud)

但是,如果我创造identity,而不是

identity = FactoryGirl.create(:identity)
Run Code Online (Sandbox Code Playgroud)

identity工厂将首先尝试创建父user,这将反过来创建另一个identity.当它最终回到创建identity我进行调用时,另一个identity已经存在并且相同provider,user并且它失败了.

从本质上讲,我需要一种方法让after(:create)回调user:identity工厂创建时不会触发.有没有办法告诉是什么让这个电话创建了一个特定的工厂?

Fin*_*inn 10

正如Dave所指出的,使用瞬态属性是一种选择.另一种选择是nil在构建相关工厂时通过.

FactoryGirl:避免关联之间的循环/无限循环

让我用一个例子来说明:

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 
Run Code Online (Sandbox Code Playgroud)

当我们调用时build(:user),代码最终到达以下行:

identity { build(:identity, user: nil) }
Run Code Online (Sandbox Code Playgroud)

这称为身份工厂.当它到达通常构建用户关联(user { build(:user, identity: nil) })的行时,它会跳过它,因为用户已经设置(为零).恭喜你,你只是避免了循环依赖!

当你打电话时,它的工作方式相同build(:identity).


FactoryGirl:访问相关工厂中一个工厂的属性

还有一件事:在您的情况下,您需要访问身份工厂中用户的电子邮件属性.在您的代码示例中,您说:

factory :identity do
  ...
  email { user.email }
end
Run Code Online (Sandbox Code Playgroud)

显然,build(:user)当我们调用身份工厂时,因为我们将用户设置为nil ,所以这会失败.不要害怕!当我们调用身份工厂时,我们只是通过电子邮件传递一个新的用户对象.这条线变成:

identity { build(:identity, user: User.new(email: email)) }
Run Code Online (Sandbox Code Playgroud)

这将阻止循环,无限关联循环以及确保标识工厂中的电子邮件属性可用.

最后,您的代码将如下所示:

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 
Run Code Online (Sandbox Code Playgroud)

希望它有用!


Dav*_*uth 5

我认为没有一个好的方法可以让一个工厂在没有协作的情况下判断它已被另一个工厂调用。(您始终可以检查caller_locations,但这并不好。)相反,让一个工厂使用瞬态属性告诉另一个工厂采取不同的行为:

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
Run Code Online (Sandbox Code Playgroud)