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.