factory_bot icon indicating copy to clipboard operation
factory_bot copied to clipboard

Add find_or_create_by Method

Open hatsu38 opened this issue 1 year ago • 3 comments

Problem this feature will solve

When setting up test data with FactoryBot, it can be cumbersome and repetitive to check if a record exists before creating it. For instance, in our current test setup, we have to manually use find_by followed by create if the record is not found.

RSpec.describe User do 
  before(:all) { Fixtures.setup_basics }
  
  it do
    # aa
  end
  
  it do
    # aa
  end
end


module Fixtures
  def self.setup_basics
    setup_normal_plan
    setup_high_plan
  end

  def self.normal_plan
      a_token = FactoryBot.create(:token :free)
      b_token = FactoryBot.create(:token, :normal)
     # etc...
  end

  def self.high_plan
    a_token = Token.find_by(name: "free") || FactoryBot.create(:token :free)
    b_token = Token.find_by(name: "normal") || FactoryBot.create(:token :normal)
  end
end

Desired solution

  def self.high_plan
    a_token = Token.find_or_create_by(:token :free)
    b_token = Token.find_or_create_by(:token :normal)
  end

Alternatives considered

  def self.high_plan
    a_token = Token.find_by(name: "free") || FactoryBot.create(:token :free)
    b_token = Token.find_by(name: "normal") || FactoryBot.create(:token :normal)
  end

Additional context

hatsu38 avatar Jul 20 '24 11:07 hatsu38

I believe that is somewhat intended. Sharing data between test runs is usually a code smell, either in the runtime code or your testing code.

Assuming you have some level of cleanup strategy, why not just create a new token each time? is the name a hardcoded value in your runtime code? if the object is highly complex and/or for instance, provided by an external service, have you thought of using mocks?

I get what you are asking for here, but passive (ab)use of a Find_or_create (in a test suite for actor objects) often leads to flaky test suites.

colinross avatar Oct 04 '24 00:10 colinross

We need find_or_create_by in FactoryBot because we use it to generate seeds as well, not just test data. It wasn't my idea to use it in seeds, but someone on my team did, and without find_or_create_by, it becomes hard to make seeds idempotent.

AndyObtiva avatar Jul 24 '25 15:07 AndyObtiva

An alternative is to define an initialize_with callback in the factory you want to find or create:

factory :state do
  abbreviation { 'AL' }
  name { 'Alagoas' }

  initialize_with { State.find_by(abbreviation:) || new(attributes) }
end

Note that all the other attributes will be ignored if the record is found.

neilvcarvalho avatar Jul 24 '25 18:07 neilvcarvalho