factory_bot Rails: Test Data Done Right
factory_bot (formerly FactoryGirl) is the standard library for managing test data in Ruby on Rails applications. Instead of manually building objects in each test, you define factories once and reuse them everywhere. The result: cleaner tests, consistent data, and less setup noise.
Why factory_bot?
Consider two approaches to building a test user:
Without factory_bot:
user = User.create!(
name: 'Alice',
email: 'alice@example.com',
password: 'password123',
role: 'admin',
confirmed_at: Time.current,
organization: Organization.create!(name: 'ACME')
)With factory_bot:
user = create(:user, :admin)The factory hides all the irrelevant details. Each test only specifies what matters for that test.
Installation
# Gemfile
group :development, :test do
gem 'factory_bot_rails'
endbundle installAdd the syntax helper to spec/support/factory_bot.rb:
RSpec.configure do |config|
config.include FactoryBot::Syntax::Methods
endDefining Factories
Factories live in spec/factories/. Create one file per model:
# spec/factories/users.rb
FactoryBot.define do
factory :user do
name { 'Alice Smith' }
email { 'alice@example.com' }
password { 'password123' }
role { 'member' }
active { true }
end
endFor unique fields, use sequences:
FactoryBot.define do
factory :user do
sequence(:name) { |n| "User #{n}" }
sequence(:email) { |n| "user#{n}@example.com" }
password { 'password123' }
role { 'member' }
active { true }
end
endSequences ensure every call to create(:user) produces a unique email. This prevents unique constraint violations in tests that create multiple users.
Building vs Creating
factory_bot provides multiple strategies:
# build — instantiates the object without saving to the database
user = build(:user)
user.persisted? # => false
# create — saves to the database
user = create(:user)
user.persisted? # => true
# build_stubbed — creates a stub with a fake id (no DB hit)
user = build_stubbed(:user)
user.id # => some fake integer
user.persisted? # => true (but not in DB)
# attributes_for — returns a hash of attributes (useful for controller tests)
attrs = attributes_for(:user)
# => { name: 'Alice Smith', email: 'user1@example.com', ... }Prefer build over create when you don't need database persistence. Tests using build run faster.
Traits
Traits let you define named variants of a factory:
FactoryBot.define do
factory :user do
sequence(:email) { |n| "user#{n}@example.com" }
name { 'Alice' }
role { 'member' }
active { true }
trait :admin do
role { 'admin' }
end
trait :inactive do
active { false }
end
trait :confirmed do
confirmed_at { Time.current }
end
end
endApply traits when creating:
create(:user, :admin)
create(:user, :inactive)
create(:user, :admin, :confirmed) # multiple traitsTraits keep your factories DRY. Instead of defining factory :admin_user, just add an :admin trait to the base factory.
Associations
When a model belongs to another, define the association in the factory:
FactoryBot.define do
factory :post do
title { 'My First Post' }
body { 'Content goes here.' }
published { false }
association :author, factory: :user
end
endfactory_bot automatically creates the associated user when you create(:post).
If the association name matches the factory name:
factory :post do
title { 'My Post' }
user # shorthand for association :user, factory: :user
endAvoiding Extra Database Records
When testing associations, you sometimes want to provide an existing object instead of creating a new one:
let(:author) { create(:user) }
let(:post) { create(:post, author: author) }
it 'shows the author name' do
expect(post.author.name).to eq(author.name)
endThis prevents factory_bot from creating an extra user.
Nested Associations
FactoryBot.define do
factory :comment do
body { 'Great post!' }
association :post
# post's factory will create its own :user association
end
endBe careful with deeply nested associations — they can create many database records per test.
Callbacks
Use after(:build) and after(:create) for side effects:
FactoryBot.define do
factory :user do
sequence(:email) { |n| "user#{n}@example.com" }
name { 'Alice' }
after(:create) do |user|
create(:profile, user: user)
end
end
endUse callbacks sparingly. Excessive after-create hooks slow down tests and can cause surprising side effects.
create_list and build_list
Create multiple records at once:
# Create 5 users
users = create_list(:user, 5)
# Build 3 users with overrides
users = build_list(:user, 3, active: false)
# Create 5 users with a trait
users = create_list(:user, 5, :admin)Useful for testing pagination, bulk operations, or list displays.
Lazy vs Eager Attributes
By default, factory_bot evaluates attributes lazily when the factory is called:
factory :event do
name { 'Conference' }
starts_at { 1.week.from_now } # evaluated when factory is called
endThis ensures starts_at is relative to when the test runs, not when the factory was defined.
Static attributes (without a block) are evaluated once:
factory :event do
name 'Conference' # evaluated once at load time
endFor time-sensitive tests, always use blocks.
Custom Factories Per Test Context
You can create "inline" factories for one-off cases:
it 'shows the most recent post at the top' do
old_post = create(:post, created_at: 2.days.ago)
new_post = create(:post, created_at: 1.hour.ago)
expect(Post.recent.first).to eq(new_post)
endOverride specific attributes inline rather than creating a new named factory.
Testing with factory_bot
Keep factories minimal
Define only what the model requires. Let tests set the relevant attributes:
# Good — minimal factory
factory :user do
sequence(:email) { |n| "user#{n}@example.com" }
password { 'password' }
end
# Bad — factory full of arbitrary defaults that leak into tests
factory :user do
name { 'Alice Smith' }
email { 'alice@acme.com' }
age { 30 }
city { 'New York' }
bio { 'Software developer' }
# ...
endToo many defaults make tests harder to read — the relevant data drowns in noise.
Don't share mutable factory output
# Bad — same object shared between examples (mutable state)
let(:user) { create(:user) }
subject { user }
# Good — fresh object per example
let(:user) { create(:user) }let already creates a fresh object per example, so you're fine here. The problem arises with before(:all) or let! holding mutable objects across examples.
Use build_stubbed for fast unit tests
When your unit under test doesn't need DB records:
let(:user) { build_stubbed(:user, :admin) }
it 'returns the correct display name' do
expect(user.display_name).to eq('Alice (Admin)')
endbuild_stubbed runs 10-100x faster than create for large test suites.
Linting Factories
factory_bot includes a lint task that creates every factory and checks for validity:
# spec/factories_spec.rb
RSpec.describe 'FactoryBot factories' do
it 'are all valid' do
FactoryBot.lint
end
endOr with traits:
FactoryBot.lint traits: trueRun this in CI to catch broken factories before they break individual tests.
Organizing Factory Files
For large projects, keep factories in spec/factories/ with one file per model:
spec/factories/
users.rb
posts.rb
comments.rb
organizations.rbYou can also use subdirectories for namespaced models:
spec/factories/
admin/
users.rb
billing/
invoices.rbA clean factory file is short, readable, and expresses the default state of a model. Complex variants go in traits. One-off overrides go in individual test files.