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
endFactoryBot 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
endThis 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
endThe 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 }
endFaker 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 # => nilUse 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 # => 1Use 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
endEach 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
endTraits
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
endUse 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
endWhen 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
enduser = create(:user, :with_articles)
user.articles.count # => 3Lazy 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 }
endLazy 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
endThis 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
enduser = create(:user, article_count: 5)
user.articles.count # => 5Transient 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
endBe 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
endcreate(: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
endFactoryBot.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: trueCommon 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_validDon'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 usedDon'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 }
endOrganizing Factories
For large applications, keep one factory file per model:
spec/factories/
users.rb
articles.rb
orders.rb
order_items.rb
products.rbFor 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
endWell-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.