通过factory_girl协会查找或创建记录

发布于 2024-12-01 01:03:14 字数 670 浏览 0 评论 0原文

我有一个属于组的用户模型。组必须具有唯一的名称属性。用户工厂和组工厂定义为:

Factory.define :user do |f|
  f.association :group, :factory => :group
  # ...
end

Factory.define :group do |f|
  f.name "default"
end

创建第一个用户时,也会创建一个新组。当我尝试创建第二个用户时,它失败了,因为它想再次创建相同的组。

有没有办法告诉factory_girl关联方法首先查找现有记录?

注意:我确实尝试定义一个方法来处理这个问题,但是我无法使用 f.association。我希望能够在像这样的 Cucumber 场景中使用它:

Given the following user exists:
  | Email          | Group         |
  | [email protected] | Name: mygroup |

并且只​​有在工厂定义中使用关联时这才有效。

I have a User model that belongs to a Group. Group must have unique name attribute. User factory and group factory are defined as:

Factory.define :user do |f|
  f.association :group, :factory => :group
  # ...
end

Factory.define :group do |f|
  f.name "default"
end

When the first user is created a new group is created too. When I try to create a second user it fails because it wants to create same group again.

Is there a way to tell factory_girl association method to look first for an existing record?

Note: I did try to define a method to handle this, but then I cannot use f.association. I would like to be able to use it in Cucumber scenarios like this:

Given the following user exists:
  | Email          | Group         |
  | [email protected] | Name: mygroup |

and this can only work if association is used in Factory definition.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(9

你不是我要的菜∠ 2024-12-08 01:03:14

您可以将 initialize_withfind_or_create 方法一起使用

FactoryGirl.define do
  factory :group do
    name "name"
    initialize_with { Group.find_or_create_by_name(name)}
  end

  factory :user do
    association :group
  end
end

它也可以与 id 一起使用

FactoryGirl.define do
  factory :group do
    id     1
    attr_1 "default"
    attr_2 "default"
    ...
    attr_n "default"
    initialize_with { Group.find_or_create_by_id(id)}
  end

  factory :user do
    association :group
  end
end

对于 Rails 4

Rails 4 中正确的方法是 Group .find_or_create_by(name: name),因此您可以使用

initialize_with { Group.find_or_create_by(name: name) } 

它。

You can to use initialize_with with find_or_create method

FactoryGirl.define do
  factory :group do
    name "name"
    initialize_with { Group.find_or_create_by_name(name)}
  end

  factory :user do
    association :group
  end
end

It can also be used with id

FactoryGirl.define do
  factory :group do
    id     1
    attr_1 "default"
    attr_2 "default"
    ...
    attr_n "default"
    initialize_with { Group.find_or_create_by_id(id)}
  end

  factory :user do
    association :group
  end
end

For Rails 4

The correct way in Rails 4 is Group.find_or_create_by(name: name), so you'd use

initialize_with { Group.find_or_create_by(name: name) } 

instead.

无所谓啦 2024-12-08 01:03:14

我最终使用了在网络上找到的多种方法,其中一种是继承工厂,正如 duckyfuzz 在另一个答案中所建议的那样。

我做了以下事情:

# in groups.rb factory

def get_group_named(name)
  # get existing group or create new one
  Group.where(:name => name).first || Factory(:group, :name => name)
end

Factory.define :group do |f|
  f.name "default"
end

# in users.rb factory

Factory.define :user_in_whatever do |f|
  f.group { |user| get_group_named("whatever") }
end

I ended up using a mix of methods found around the net, one of them being inherited factories as suggested by duckyfuzz in another answer.

I did following:

# in groups.rb factory

def get_group_named(name)
  # get existing group or create new one
  Group.where(:name => name).first || Factory(:group, :name => name)
end

Factory.define :group do |f|
  f.name "default"
end

# in users.rb factory

Factory.define :user_in_whatever do |f|
  f.group { |user| get_group_named("whatever") }
end
嘿哥们儿 2024-12-08 01:03:14

您还可以使用 FactoryGirl 策略来实现此目的。

module FactoryGirl
  module Strategy
    class Find
      def association(runner)
        runner.run
      end

      def result(evaluation)
        build_class(evaluation).where(get_overrides(evaluation)).first
      end

      private

      def build_class(evaluation)
        evaluation.instance_variable_get(:@attribute_assigner).instance_variable_get(:@build_class)
      end

      def get_overrides(evaluation = nil)
        return @overrides unless @overrides.nil?
        evaluation.instance_variable_get(:@attribute_assigner).instance_variable_get(:@evaluator).instance_variable_get(:@overrides).clone
      end
    end

    class FindOrCreate
      def initialize
        @strategy = FactoryGirl.strategy_by_name(:find).new
      end

      delegate :association, to: :@strategy

      def result(evaluation)
        found_object = @strategy.result(evaluation)

        if found_object.nil?
          @strategy = FactoryGirl.strategy_by_name(:create).new
          @strategy.result(evaluation)
        else
          found_object
        end
      end
    end
  end

  register_strategy(:find, Strategy::Find)
  register_strategy(:find_or_create, Strategy::FindOrCreate)
end

您可以使用此要点
然后执行以下操作

FactoryGirl.define do
  factory :group do
    name "name"
  end

  factory :user do
    association :group, factory: :group, strategy: :find_or_create, name: "name"
  end
end

,但这对我有用。

You can also use a FactoryGirl strategy to achieve this

module FactoryGirl
  module Strategy
    class Find
      def association(runner)
        runner.run
      end

      def result(evaluation)
        build_class(evaluation).where(get_overrides(evaluation)).first
      end

      private

      def build_class(evaluation)
        evaluation.instance_variable_get(:@attribute_assigner).instance_variable_get(:@build_class)
      end

      def get_overrides(evaluation = nil)
        return @overrides unless @overrides.nil?
        evaluation.instance_variable_get(:@attribute_assigner).instance_variable_get(:@evaluator).instance_variable_get(:@overrides).clone
      end
    end

    class FindOrCreate
      def initialize
        @strategy = FactoryGirl.strategy_by_name(:find).new
      end

      delegate :association, to: :@strategy

      def result(evaluation)
        found_object = @strategy.result(evaluation)

        if found_object.nil?
          @strategy = FactoryGirl.strategy_by_name(:create).new
          @strategy.result(evaluation)
        else
          found_object
        end
      end
    end
  end

  register_strategy(:find, Strategy::Find)
  register_strategy(:find_or_create, Strategy::FindOrCreate)
end

You can use this gist.
And then do the following

FactoryGirl.define do
  factory :group do
    name "name"
  end

  factory :user do
    association :group, factory: :group, strategy: :find_or_create, name: "name"
  end
end

This is working for me, though.

云雾 2024-12-08 01:03:14

我遇到了类似的问题并提出了这个解决方案。它按名称查找组,如果找到,则将用户与该组关联起来。否则,它会按该名称创建一个组,然后与其关联。

factory :user do
  group { Group.find_by(name: 'unique_name') || FactoryBot.create(:group, name: 'unique_name') }
end

我希望这对某人有用:)

I had a similar problem and came up with this solution. It looks for a group by name and if it is found it associates the user with that group. Otherwise it creates a group by that name and then associates with it.

factory :user do
  group { Group.find_by(name: 'unique_name') || FactoryBot.create(:group, name: 'unique_name') }
end

I hope this can be useful to someone :)

看春风乍起 2024-12-08 01:03:14

为了确保 FactoryBot 的 buildcreate 仍然按其应有的方式运行,我们应该只覆盖 create 的逻辑,方法是

factory :user do
  association :group, factory: :group
  # ...
end

factory :group do
  to_create do |instance|
    instance.id = Group.find_or_create_by(name: instance.name).id
    instance.reload
  end

  name { "default" }
end

: build 保持“构建/初始化对象”的默认行为,并且不执行任何数据库读取或写入,因此它总是很快。仅覆盖 create 的逻辑以获取现有记录(如果存在),而不是尝试始终创建新记录。

我写了一篇文章来解释这一点。

To ensure FactoryBot's build and create still behaves as it should, we should only override the logic of create, by doing:

factory :user do
  association :group, factory: :group
  # ...
end

factory :group do
  to_create do |instance|
    instance.id = Group.find_or_create_by(name: instance.name).id
    instance.reload
  end

  name { "default" }
end

This ensures build maintains it's default behavior of "building/initializing the object" and does not perform any database read or write so it's always fast. Only logic of create is overridden to fetch an existing record if exists, instead of attempting to always create a new record.

I wrote an article explaining this.

你没皮卡萌 2024-12-08 01:03:14

我一直在寻找一种不影响工厂的方法。正如 @Hiasinho 所指出的,创建策略是正确的方法。然而,该解决方案不再适合我,可能是 API 发生了变化。想出了这个:

module FactoryBot
  module Strategy
    # Does not work when passing objects as associations: `FactoryBot.find_or_create(:entity, association: object)`
    # Instead do: `FactoryBot.find_or_create(:entity, association_id: id)`
    class FindOrCreate
      def initialize
        @build_strategy = FactoryBot.strategy_by_name(:build).new
      end

      delegate :association, to: :@build_strategy

      def result(evaluation)
        attributes = attributes_shared_with_build_result(evaluation)
        evaluation.object.class.where(attributes).first || FactoryBot.strategy_by_name(:create).new.result(evaluation)
      end

      private

      # Here we handle possible mismatches between initially provided attributes and actual model attrbiutes
      # For example, devise's User model is given a `password` and generates an `encrypted_password`
      # In this case, we shouldn't use `password` in the `where` clause
      def attributes_shared_with_build_result(evaluation)
        object_attributes = evaluation.object.attributes
        evaluation.hash.filter { |k, v| object_attributes.key?(k.to_s) }
      end
    end
  end

  register_strategy(:find_or_create, Strategy::FindOrCreate)
end

并像这样使用它:

org = FactoryBot.find_or_create(:organization, name: 'test-org')
user = FactoryBot.find_or_create(:user, email: '[email protected]', password: 'test', organization: org)

I was looking for a way that doesn't affect the factories. Creating a Strategy is the way to go, as pointed out by @Hiasinho. However, that solution didn't work for me anymore, probably the API changed. Came up with this:

module FactoryBot
  module Strategy
    # Does not work when passing objects as associations: `FactoryBot.find_or_create(:entity, association: object)`
    # Instead do: `FactoryBot.find_or_create(:entity, association_id: id)`
    class FindOrCreate
      def initialize
        @build_strategy = FactoryBot.strategy_by_name(:build).new
      end

      delegate :association, to: :@build_strategy

      def result(evaluation)
        attributes = attributes_shared_with_build_result(evaluation)
        evaluation.object.class.where(attributes).first || FactoryBot.strategy_by_name(:create).new.result(evaluation)
      end

      private

      # Here we handle possible mismatches between initially provided attributes and actual model attrbiutes
      # For example, devise's User model is given a `password` and generates an `encrypted_password`
      # In this case, we shouldn't use `password` in the `where` clause
      def attributes_shared_with_build_result(evaluation)
        object_attributes = evaluation.object.attributes
        evaluation.hash.filter { |k, v| object_attributes.key?(k.to_s) }
      end
    end
  end

  register_strategy(:find_or_create, Strategy::FindOrCreate)
end

And use it like this:

org = FactoryBot.find_or_create(:organization, name: 'test-org')
user = FactoryBot.find_or_create(:user, email: '[email protected]', password: 'test', organization: org)
沙与沫 2024-12-08 01:03:14

通常我只是做多个工厂定义。一种用于有组的用户,一种用于无组的用户:

Factory.define :user do |u|
  u.email "email"
  # other attributes
end

Factory.define :grouped_user, :parent => :user do |u|
  u.association :group
  # this will inherit the attributes of :user
end

然后您可以在步骤定义中使用这些来单独创建用户和组,并随意将它们连接在一起。例如,您可以创建一名分组用户和一名单独用户,并将单独用户加入分组用户团队。

不管怎样,你应该看看 pickle gem ,它允许你编写如下步骤:

Given a user exists with email: "[email protected]"
And a group exists with name: "default"
And the user: "[email protected]" has joined that group
When somethings happens....

Usually I just make multiple factory definitions. One for a user with a group and one for a groupless user:

Factory.define :user do |u|
  u.email "email"
  # other attributes
end

Factory.define :grouped_user, :parent => :user do |u|
  u.association :group
  # this will inherit the attributes of :user
end

THen you can use these in your step definitions to create users and groups seperatly and join them together at will. For example you could create one grouped user and one lone user and join the lone user to the grouped users team.

Anyway, you should take a look at the pickle gem which will allow you to write steps like:

Given a user exists with email: "[email protected]"
And a group exists with name: "default"
And the user: "[email protected]" has joined that group
When somethings happens....
墟烟 2024-12-08 01:03:14

我正在使用您在问题中描述的 Cucumber 场景:

Given the following user exists:
  | Email          | Group         |
  | [email protected] | Name: mygroup |

您可以将其扩展为:

Given the following user exists:
  | Email          | Group         |
  | [email protected] | Name: mygroup |
  | [email protected]  | Name: mygroup |
  | [email protected]  | Name: mygroup |

这将创建 3 个具有“mygroup”组的用户。由于它像这样使用“find_or_create_by”功能,因此第一个调用创建组,接下来的两个调用查找已创建的组。

I'm using exactly the Cucumber scenario you described in your question:

Given the following user exists:
  | Email          | Group         |
  | [email protected] | Name: mygroup |

You can extend it like:

Given the following user exists:
  | Email          | Group         |
  | [email protected] | Name: mygroup |
  | [email protected]  | Name: mygroup |
  | [email protected]  | Name: mygroup |

This will create 3 users with the group "mygroup". As it used like this uses 'find_or_create_by' functionality, the first call creates the group, the next two calls finds the already created group.

骑趴 2024-12-08 01:03:14

另一种方法(适用于任何属性并适用于关联):

# config/initializers/factory_bot.rb
#
# Example use:
#
# factory :my_factory do
#   change_factory_to_find_or_create
#
#   some_attr { 7 }
#   other_attr { "hello" }
# end
#
# FactoryBot.create(:my_factory) # creates
# FactoryBot.create(:my_factory) # finds
# FactoryBot.create(:my_factory, other_attr: "new value") # creates
# FactoryBot.create(:my_factory, other_attr: "new value") # finds

module FactoryBotEnhancements
  def change_factory_to_find_or_create
    to_create do |instance|
      # Note that this will ignore nil value attributes, to avoid auto-generated attributes such as id and timestamps
      attributes = instance.class.find_or_create_by(instance.attributes.compact).attributes
      instance.attributes = attributes.except('id')
      instance.id = attributes['id'] # id can't be mass-assigned
      instance.instance_variable_set('@new_record', false) # marks record as persisted
    end
  end
end

# This makes the module available to all factory definition blocks
class FactoryBot::DefinitionProxy
  include FactoryBotEnhancements
end

唯一的警告是您无法通过 nil 值找到。除此之外,一切就像做梦一样

Another way to do it (that will work with any attribute and work with associations):

# config/initializers/factory_bot.rb
#
# Example use:
#
# factory :my_factory do
#   change_factory_to_find_or_create
#
#   some_attr { 7 }
#   other_attr { "hello" }
# end
#
# FactoryBot.create(:my_factory) # creates
# FactoryBot.create(:my_factory) # finds
# FactoryBot.create(:my_factory, other_attr: "new value") # creates
# FactoryBot.create(:my_factory, other_attr: "new value") # finds

module FactoryBotEnhancements
  def change_factory_to_find_or_create
    to_create do |instance|
      # Note that this will ignore nil value attributes, to avoid auto-generated attributes such as id and timestamps
      attributes = instance.class.find_or_create_by(instance.attributes.compact).attributes
      instance.attributes = attributes.except('id')
      instance.id = attributes['id'] # id can't be mass-assigned
      instance.instance_variable_set('@new_record', false) # marks record as persisted
    end
  end
end

# This makes the module available to all factory definition blocks
class FactoryBot::DefinitionProxy
  include FactoryBotEnhancements
end

The only caveat is that you can't find by nil values. Other than that, it works like a dream

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文