factory_bot Rails: Test Data Done Right

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'
end
bundle install

Add the syntax helper to spec/support/factory_bot.rb:

RSpec.configure do |config|
  config.include FactoryBot::Syntax::Methods
end

Defining 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
end

For 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
end

Sequences 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
end

Apply traits when creating:

create(:user, :admin)
create(:user, :inactive)
create(:user, :admin, :confirmed)  # multiple traits

Traits 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
end

factory_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
end

Avoiding 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)
end

This 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
end

Be 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
end

Use 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
end

This 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
end

For 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)
end

Override 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' }
  # ...
end

Too 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)')
end

build_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
end

Or with traits:

FactoryBot.lint traits: true

Run 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.rb

You can also use subdirectories for namespaced models:

spec/factories/
  admin/
    users.rb
  billing/
    invoices.rb

A 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.

Read more