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

时间:2011-08-22 09:10:15

标签: ruby ruby-on-rails-3 cucumber factory-bot

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

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         |
  | test@email.com | Name: mygroup |

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

8 个答案:

答案 0 :(得分:100)

您可以initialize_with使用find_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) } 

代替。

答案 1 :(得分:16)

我最终使用网络上发现的各种方法,其中一种是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

答案 2 :(得分:6)

您还可以使用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

您可以使用this gist。 然后执行以下操作

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

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

但这对我有用。

答案 3 :(得分:2)

通常我只会制作多个工厂定义。一个用于具有组的用户,一个用于无组用户:

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: "hello@email.com"
And a group exists with name: "default"
And the user: "hello@gmail.com" has joined that group
When somethings happens....

答案 4 :(得分:0)

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

Given the following user exists:
  | Email          | Group         |
  | test@email.com | Name: mygroup |

您可以将其扩展为:

Given the following user exists:
  | Email          | Group         |
  | test@email.com | Name: mygroup |
  | foo@email.com  | Name: mygroup |
  | bar@email.com  | Name: mygroup |

这将使用“mygroup”组创建3个用户。正如它使用'find_or_create_by'功能一样,第一个调用创建组,接下来的两个调用查找已创建的组。

答案 5 :(得分:0)

我最近遇到了类似的问题,这就是我尝试过的事情。

为确保FactoryBot的buildcreate仍能正常工作,我们应该通过执行以下操作来仅覆盖create的逻辑:

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

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

  name { "default" }
end

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

我写了an article对此进行了解释。

答案 6 :(得分:0)

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

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

我希望这对某人有用:)

答案 7 :(得分:0)

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

# 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 值找到。除此之外,它就像做梦一样