Find or create record through factory_girl association
Solution 1:
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.
Solution 2:
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
Solution 3:
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.
Solution 4:
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 :)
Solution 5:
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.