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

How to avoid circular creation of associated models in factory_girl?

我有一个应用程序,用户可以在其中使用多种服务登录,例如Google 另外,脸书、推特等

为方便起见,我有一个基础 User 模型,其中 has_many Identity 记录。

我的工厂设置如下:

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

User 模型有一个创建 Identity 记录的回调。当 运行

时效果很好
user = FactoryGirl.create(:user)

但是,如果我改为创建 identity

identity = FactoryGirl.create(:identity)

identity 工厂将首先尝试创建一个父 user,这将依次创建另一个 identity。当它最终回到创建我调用的 identity 时,另一个 identity 已经存在,并且 user 具有相同的 provider 并且它失败了。

本质上,我需要一种方法让 after(:create) 回调在 :identity 工厂创建 user 时不触发。有没有办法知道是什么调用创建了一个特定的工厂?

我不认为有什么好的方法可以让工厂知道它是在没有协作的情况下被另一个工厂调用的。 (您始终可以检查 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

正如 Dave 指出的那样,使用瞬态属性是一种选择。另一种选择是在构建关联工厂时传递nil

FactoryGirl:避免circular/infinite 关联之间的循环

让我举个例子:

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 

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

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

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

当您调用 build(:identity) 时,它的工作方式相同。


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

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

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

显然,当我们调用 build(:user) 时失败了,因为我们在调用身份工厂时将 user 设置为 nil。不要怕!当我们调用身份工厂时,我们只需传递一个带有电子邮件的新用户对象。所以该行变为:

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

这将防止循环、无限关联循环,并确保电子邮件属性在身份工厂中可用。

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

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 

希望对您有所帮助!