通过工厂女孩协会查找或创建记录

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

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


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

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

有没有一种方法可以告诉 Factory _ girl 关联方法首先查找现有记录?

注意: 我确实尝试定义一个方法来处理这个问题,但是我不能使用 f.association。我希望能够在黄瓜场景中使用它,如下所示:

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

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

29461 次浏览

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

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

然后,您可以在步骤定义中使用它们分别创建用户和组,并随意将它们联接在一起。例如,您可以创建一个分组用户和一个单独的用户,并将单独的用户加入到分组用户团队中。

无论如何,你应该看一下 泡菜宝石,它允许你写下这样的步骤:

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....

我最终使用了网络上找到的各种方法,其中之一就是 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

我使用的正是你在问题中描述的黄瓜场景:

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 |

这将创建3个用户与组“ mygroup”。像这样使用“ find _ or _ create _ by”功能,第一个调用创建组,接下来的两个调用找到已经创建的组。

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

取而代之。

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

不过这招对我挺管用的。

为了确保 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的逻辑被重写以获取现有记录(如果存在) ,而不是尝试始终创建新记录。

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

我也遇到过类似的问题,于是想出了这个解决方案。它按名称查找组,如果找到了,它将用户与该组关联起来。否则,它将根据该名称创建一个组,然后与之关联。

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

我希望这对某些人有用:)

我在寻找一种不影响工厂的方法。正如@Hiasinho 所指出的,创建 Strategy才是正确的做法。然而,那个解决方案不再适合我了,可能 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: 'test@test.com', password: 'test', organization: org)

另一种方法(可以处理任何属性和关联) :

# 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

唯一需要注意的是,你不能通过空值来求解。除此之外,它的工作原理就像一个梦