Add find_or_create_by Method
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
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.
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.
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.