FactoryBot testing guidelines
Last Updated: July 9, 2025
FactoryBot is a library for building test data objects in Ruby applications. This guide covers best practices for creating maintainable, reliable factories and using them effectively in your test suite.
Philosophy
Factories should be pure data builders that create valid objects with sensible defaults. They shouldn't contain business logic, external service calls, or test setup code. This separation keeps factories reusable across different test scenarios.
Basic usage
Creating factories
# spec/factories/users.rb
FactoryBot.define do
factory :user do
first_name { 'John' }
last_name { 'Doe' }
sequence(:email) { |n| "user#{n}@example.com" }
sequence(:ssn) { |n| "123-45-#{n.to_s.rjust(4, '0')}" }
end
end
Using factories in tests
In spec files, you can use these methods to create objects:
# In spec files
user = create(:user) # Persisted object
user = build(:user) # Non-persisted object
user = build_stubbed(:user) # Stubbed object (faster)
user = create(:user, first_name: 'Jane') # Override attributes
Method availability
In spec files: Use shorthand
build(:user)
andcreate(:user)
In support files: Use explicit
FactoryBot.build(:user)
andFactoryBot.create(:user)
These shorthands work in any file where you include FactoryBot::Syntax::Methods
. Many Vets-API modules enable it globally.
Advanced usage
Traits
Use traits to create variations of a factory:
factory :user do
first_name { 'John' }
last_name { 'Doe' }
trait :admin do
admin { true }
end
trait :with_benefits do
benefits_status { 'active' }
end
end
Then use traits in your tests:
admin_user = create(:user, :admin)
user_with_benefits = create(:user, :with_benefits)
Associations
Handle relationships between objects:
factory :claim do
user
status { 'pending' }
# Explicit association
factory :claim_with_user do
association :user, factory: :user, first_name: 'Jane'
end
end
Sequences
Generate unique values:
factory :user do
sequence(:email) { |n| "user#{n}@example.com" }
sequence(:ssn) { |n| "123-45-#{n.to_s.rjust(4, '0')}" }
end
Callbacks
Use callbacks sparingly and only for data setup:
factory :user do
first_name { 'John' }
after(:build) do |user|
user.full_name = "#{user.first_name} #{user.last_name}"
end
end
Additional methods
attributes_for
Get a hash of attributes without creating an object:
attrs = attributes_for(:user)
# => { first_name: 'John', last_name: 'Doe', email: 'user1@example.com' }
create_list
and build_list
Create multiple objects at once:
users = create_list(:user, 3)
claims = build_list(:claim, 5, user: user)
transient
attributes
Use temporary attributes that won't be passed to the model:
factory :user do
transient do
claim_count { 2 }
end
first_name { 'John' }
after(:create) do |user, evaluator|
create_list(:claim, evaluator.claim_count, user: user)
end
end
Best practices
1. Keep factories simple
Use sensible defaults that work for most tests
Avoid complex calculations or external dependencies
Focus on creating valid, minimal objects
Here's how to create simple, effective factories:
# Good
factory :user do
first_name { 'John' }
last_name { 'Doe' }
sequence(:email) { |n| "user#{n}@example.com" }
end
# Avoid
factory :user do
first_name { calculate_random_name_from_api }
complex_field { perform_expensive_operation }
end
2. Use traits for variations
Instead of creating many similar factories, use traits:
# Good
factory :user do
first_name { 'John' }
trait :admin do
role { 'admin' }
end
trait :veteran do
military_service { true }
end
end
# Less ideal
factory :admin_user do
first_name { 'John' }
role { 'admin' }
end
factory :veteran_user do
first_name { 'John' }
military_service { true }
end
3. Separate test setup from data creation
Keep stubbing and mocking in your test files, not in factories:
# In your spec file
describe UserService do
let(:user) { build(:user) }
before do
stub_mpi(user.icn)
allow(ExternalService).to receive(:call)
end
end
4. Use build_stubbed for performance
When you don't need database persistence:
# Faster - no database interaction
user = build_stubbed(:user)
# Slower - saves to database
user = create(:user)
Objects returned by build_stubbed
are read-only—calling save
or update
raises RuntimeError
.
5. Override attributes explicitly
Make test intentions clear by overriding attributes:
# Clear intent
user = create(:user, first_name: 'Jane', admin: true)
# Less clear
user = create(:admin_user_named_jane)
6. Lint your factories
Add FactoryBot.lint
in a before-suite hook to catch invalid factories early:
# spec/support/factory_bot.rb
RSpec.configure do |config|
config.before(:suite) do
FactoryBot.lint
end
end
Common pitfalls
1. RSpec helpers aren't available inside factory blocks
Problem: Custom test helpers, stubs, and RSpec mock methods aren't available inside factory definitions.
This will fail:
factory :user do
after(:build) do |user|
stub_mpi(user.icn) # NoMethodError
end
end
Solution:
# Factory (clean data only)
factory :user do
first_name { 'John' }
icn { '123456789' }
end
# In spec file
let(:user) { build(:user) }
before { stub_mpi(user.icn) }
2. Method availability outside RSpec
Problem: build()
and create()
shorthand methods aren't available in support files.
This will fail:
# spec/support/test_helper.rb
def create_test_user
create(:user) # NoMethodError
end
Solution:
# spec/support/test_helper.rb
def create_test_user
FactoryBot.create(:user)
end
3. Overly complex factories
Problem: Factories that do too much become hard to maintain and debug.
Avoid this:
factory :user do
after(:create) do |user|
create_list(:claim, 5, user: user)
user.update!(status: calculate_complex_status)
send_notification_email(user)
end
end
Better approach:
factory :user do
first_name { 'John' }
status { 'active' }
end
# Create associations in tests when needed
let(:user) { create(:user) }
let(:claims) { create_list(:claim, 5, user: user) }
Troubleshooting
Common errors
Error: NoMethodError: undefined method 'stub_mpi'
Solution: Move stubbing from factory definitions to RSpec before
blocks
Error: NoMethodError: undefined method 'build'
Solution: Use FactoryBot.build
in non-spec files
Error: ArgumentError: Factory not registered
Solution: Check that factories are properly defined in spec/factories/
Debugging tips
Run individual specs to isolate factory issues
Check factory definitions in
spec/factories/
directoryVerify method availability based on file context (spec vs support)
Test factory creation in isolation:
FactoryBot.create(:user)
Summary
FactoryBot factories should be simple, focused data builders. Keep test setup, stubbing, and complex logic in your spec files where they belong. This separation leads to more maintainable tests and reusable factories.
Key principles:
Factories create data, specs handle test logic
Use explicit
FactoryBot.method
syntax outside spec filesKeep factories simple and focused
Use traits for variations, not separate factories
Help and feedback
Get help from the Platform Support Team in Slack.
Submit a feature idea to the Platform.