FactoryBot Guide: Test Data for Rails Applications

FactoryBot Guide: Test Data for Rails Applications

FactoryBot is the standard library for generating test data in Rails applications. It replaces fixtures with a programmable approach: define templates for your models, then build or create instances with exactly the attributes each test needs. This guide covers everything from basic setup to advanced patterns.

Installation and Setup

Add to your Gemfile:

group :development, :test do
  gem "factory_bot_rails"
  gem "faker"  # for realistic fake data
end

FactoryBot reads factory definitions from spec/factories/ (or test/factories/ for Minitest). Rails generates factory files automatically when you run rails generate model.

To use FactoryBot methods without the FactoryBot. prefix in your tests:

# spec/rails_helper.rb
RSpec.configure do |config|
  config.include FactoryBot::Syntax::Methods
end

This lets you write create(:user) instead of FactoryBot.create(:user).

Defining Factories

Factories live in spec/factories/. One file per model is the convention:

# spec/factories/users.rb
FactoryBot.define do
  factory :user do
    name { "Alice" }
    email { "alice@example.com" }
    role { :member }
    created_at { Time.current }
  end
end

The block syntax (using { }) is required for dynamic values. Static values work too, but dynamic values ensure each generated instance can be unique:

factory :user do
  name { Faker::Name.name }
  email { Faker::Internet.email }
  bio { Faker::Lorem.paragraph }
end

Faker provides realistic fake data — names, emails, addresses, company names, lorem text. Use it to make factory-generated data look realistic.

Build Strategies

FactoryBot has four strategies for creating test objects:

build

Creates an object in memory without saving it to the database:

user = build(:user)
user.persisted?  # => false
user.id          # => nil

Use build for tests that don't need a database record — validations, method tests, anything that doesn't involve database queries.

create

Creates an object and persists it:

user = create(:user)
user.persisted?  # => true
user.id          # => 1

Use create when you need the record in the database — scope tests, association tests, request specs.

build_stubbed

Creates an object that behaves as if it were persisted, without hitting the database:

user = build_stubbed(:user)
user.persisted?  # => true
user.id          # => 1001 (stubbed ID)

build_stubbed is faster than create because there's no database I/O. Use it when your test needs a persisted-looking object but doesn't actually query the database.

attributes_for

Returns a hash of attributes:

attrs = attributes_for(:user)
# => { name: "Alice", email: "alice@example.com", role: :member }

Useful for request specs where you pass params to a controller:

post "/users", params: { user: attributes_for(:user) }

build_list and create_list

Create multiple instances at once:

users = build_list(:user, 5)
users = create_list(:user, 3, role: :admin)

Sequences

Sequences generate unique values across factory calls — essential for attributes with uniqueness constraints:

FactoryBot.define do
  factory :user do
    sequence(:email) { |n| "user#{n}@example.com" }
    sequence(:username) { |n| "user_#{n}" }
  end
end

Each call increments the counter:

create(:user).email  # => "user1@example.com"
create(:user).email  # => "user2@example.com"
create(:user).email  # => "user3@example.com"

Define global sequences for values shared across factories:

FactoryBot.define do
  sequence(:email) { |n| "person#{n}@example.com" }

  factory :user do
    email { generate(:email) }
  end

  factory :admin do
    email { generate(:email) }
  end
end

Traits

Traits define named variations of a factory. They're the primary way to handle different states of a model:

FactoryBot.define do
  factory :user do
    name { Faker::Name.name }
    sequence(:email) { |n| "user#{n}@example.com" }
    role { :member }
    confirmed_at { nil }

    trait :admin do
      role { :admin }
    end

    trait :confirmed do
      confirmed_at { 1.day.ago }
    end

    trait :with_avatar do
      after(:create) do |user|
        user.avatar.attach(
          io: File.open("spec/fixtures/files/avatar.png"),
          filename: "avatar.png",
          content_type: "image/png"
        )
      end
    end
  end
end

Use traits individually or combine them:

create(:user, :admin)
create(:user, :confirmed)
create(:user, :admin, :confirmed)
create(:user, :admin, :confirmed, :with_avatar)

Traits keep factory definitions clean. Instead of creating separate factories for every variation (:admin_user, :confirmed_user), you compose them.

Associations

Define associated records in factories:

FactoryBot.define do
  factory :article do
    title { Faker::Lorem.sentence }
    body { Faker::Lorem.paragraphs(number: 3).join("\n\n") }
    status { :draft }
    association :author, factory: :user
  end
end

When you create(:article), FactoryBot automatically creates an associated user. When you build(:article), FactoryBot uses build for associations too — no database hit.

Override associations in tests:

author = create(:user, :admin)
article = create(:article, author: author)

For has_many associations, use after(:create):

factory :user do
  name { Faker::Name.name }

  trait :with_articles do
    after(:create) do |user|
      create_list(:article, 3, author: user)
    end
  end
end
user = create(:user, :with_articles)
user.articles.count  # => 3

Lazy Attributes

Attributes wrapped in blocks are evaluated lazily — at factory call time, not at definition time:

factory :order do
  # Static value — evaluated at definition time, same for all instances
  # subtotal { 100.0 }  # DO NOT do this

  # Lazy value — evaluated each time factory is called
  subtotal { rand(10.0..500.0).round(2) }
  created_at { rand(30.days).ago }
end

Lazy attributes also have access to other attributes via self:

factory :article do
  title { Faker::Lorem.sentence }
  slug { title.parameterize }   # uses the generated title
  meta_title { title }          # same
end

This ensures consistency: the slug always derives from the title, without hardcoding values.

Transient Attributes

Transient attributes exist only within the factory — they're not set on the built object, but they can influence how other attributes are generated:

factory :user do
  transient do
    article_count { 0 }
  end

  name { Faker::Name.name }

  after(:create) do |user, evaluator|
    if evaluator.article_count > 0
      create_list(:article, evaluator.article_count, author: user)
    end
  end
end
user = create(:user, article_count: 5)
user.articles.count  # => 5

Transient attributes allow parameterized factory behavior without polluting model attributes.

Callbacks

FactoryBot provides after(:build), after(:create), after(:stub), and before(:create) callbacks:

factory :article do
  title { Faker::Lorem.sentence }
  status { :draft }

  trait :published do
    status { :published }
    published_at { 1.day.ago }

    after(:create) do |article|
      # Simulate the publish workflow if there's a job or mailer involved
      article.touch(:published_at)
    end
  end
end

Be careful with callbacks — they add implicit behavior that can make test output confusing. Prefer explicit traits over deeply nested callbacks.

Inheritance

Factories can inherit from parent factories:

factory :user do
  name { Faker::Name.name }
  sequence(:email) { |n| "user#{n}@example.com" }
  role { :member }

  factory :admin_user do
    role { :admin }
    name { "Admin #{Faker::Name.last_name}" }
  end
end

create(:admin_user) creates a user with role: :admin. Inherited factories share all parent attributes unless overridden.

Use inheritance sparingly. Traits are usually more composable:

# Prefer this
create(:user, :admin)

# Over this
create(:admin_user)

Factory Linting

FactoryBot can verify that all factories are valid — their attributes pass model validations:

# spec/support/factory_bot.rb
RSpec.configure do |config|
  config.before(:suite) do
    FactoryBot.lint
  end
end

FactoryBot.lint calls create on every factory and checks that the resulting record is valid. Run it in CI to catch factory drift when models change.

Linting all traits:

FactoryBot.lint traits: true

Common Mistakes

Don't use create when build is enough. Every create hits the database. In a large test suite, unnecessary creates add seconds to your runtime.

# Slow — creates a DB record for a validation test
user = create(:user, email: "")
expect(user).not_to be_valid

# Fast — validates in memory
user = build(:user, email: "")
expect(user).not_to be_valid

Don't share factory instances between tests. Mutable state leaks between tests.

# Bad — shared state
let(:user) { create(:user) }
let(:article) { create(:article, author: user) }

# Tests that mutate article or user will affect each other if let! is used

Don't put everything in after(:create). Callbacks that create associated records run even when you don't need them, slowing down every test that uses the factory.

Don't hardcode IDs. FactoryBot generates IDs automatically. Hardcoded IDs collide in parallel test runs.

# Bad
factory :user do
  id { 1 }
end

Organizing Factories

For large applications, keep one factory file per model:

spec/factories/
  users.rb
  articles.rb
  orders.rb
  order_items.rb
  products.rb

For complex models with many traits, group related traits together with comments:

factory :order do
  # Base attributes
  subtotal { 100.0 }
  association :user

  # Status traits
  trait :pending   { status { :pending } }
  trait :paid      { status { :paid };   paid_at { Time.current } }
  trait :cancelled { status { :cancelled }; cancelled_at { Time.current } }

  # Content traits
  trait :with_items do
    after(:create) { |order| create_list(:order_item, 3, order: order) }
  end

  trait :with_discount do
    discount_code { "SAVE10" }
    discount_amount { 10.0 }
  end
end

Well-maintained factories are a force multiplier for your test suite. They make test setup readable, keep tests focused on behavior, and let you express complex scenarios concisely. Teams that also use HelpMeTest for continuous end-to-end monitoring can verify that the same flows their factories simulate in tests continue to work correctly in production.

Read more

Start now free