Overview
Test factories provide a structured approach to creating test objects with predefined attributes and associations. Ruby's factory ecosystem centers around FactoryBot, which defines reusable blueprints for generating test data. Factories replace manual object creation with declarative definitions that specify default attribute values, associations between models, and customizable variations through traits.
The factory pattern separates test data creation from test logic, reducing duplication and improving test maintainability. FactoryBot integrates with major testing frameworks including RSpec, Minitest, and Cucumber, providing methods like create
, build
, and build_stubbed
for different object lifecycle needs.
# Define a factory
FactoryBot.define do
factory :user do
name { "John Doe" }
email { "john@example.com" }
created_at { Time.current }
end
end
# Use the factory
user = FactoryBot.create(:user)
user.name # => "John Doe"
Factories handle complex scenarios including nested associations, conditional attributes, and sequence generation. The library provides hooks for customizing object creation, callbacks for post-creation logic, and integration with Rails fixtures and database transactions.
# Factory with associations
FactoryBot.define do
factory :post do
title { "Sample Post" }
content { "Content here" }
association :author, factory: :user
trait :published do
published_at { Time.current }
status { :published }
end
end
end
post = FactoryBot.create(:post, :published)
The factory approach scales from simple attribute assignment to complex object graphs with multiple relationships, custom validation scenarios, and domain-specific test data requirements. FactoryBot's syntax supports both static values and dynamic attribute generation through lambdas, sequences, and custom methods.
Basic Usage
Factory definitions use the FactoryBot.define
block to establish object blueprints. Each factory corresponds to a class and specifies default attribute values. The factory name typically matches the target class name, with FactoryBot inferring the class from the factory identifier.
FactoryBot.define do
factory :product do
name { "Default Product" }
price { 19.99 }
category { "electronics" }
active { true }
end
factory :admin_user, class: 'User' do
name { "Admin User" }
email { "admin@company.com" }
role { "administrator" }
end
end
The primary factory methods handle different object lifecycle stages. FactoryBot.create
persists objects to the database, triggering validations and callbacks. FactoryBot.build
constructs objects in memory without database persistence, useful for testing business logic without database overhead.
# Persist to database
user = FactoryBot.create(:user)
user.persisted? # => true
# Build in memory
user = FactoryBot.build(:user)
user.persisted? # => false
user.valid? # => true
# Build with stubbed associations
user = FactoryBot.build_stubbed(:user)
user.id # => 1001 (stubbed ID)
Attribute overrides customize factory output for specific test scenarios. Override values by passing a hash to factory methods, with custom values taking precedence over factory defaults.
# Override individual attributes
user = FactoryBot.create(:user, name: "Custom Name")
user.name # => "Custom Name"
# Override multiple attributes
user = FactoryBot.create(:user,
name: "Jane Smith",
email: "jane@example.com",
created_at: 1.year.ago
)
Sequences generate unique values for attributes requiring distinctness across test runs. Define sequences using the sequence
method with increment counters or custom generation logic.
FactoryBot.define do
sequence :email do |n|
"user#{n}@example.com"
end
sequence :order_number, 1000 do |n|
"ORD-#{n}"
end
factory :customer do
name { "Customer" }
email # References the email sequence
order_number # References the order_number sequence
end
end
customer1 = FactoryBot.create(:customer)
customer1.email # => "user1@example.com"
customer2 = FactoryBot.create(:customer)
customer2.email # => "user2@example.com"
Associations between factories establish relationships between test objects. FactoryBot automatically creates associated objects when the primary object requires them, handling foreign key constraints and relationship validation.
FactoryBot.define do
factory :order do
total { 100.00 }
association :customer
# Explicit association with custom factory
association :shipping_address, factory: :address
end
factory :address do
street { "123 Main St" }
city { "Springfield" }
state { "IL" }
zip_code { "62701" }
end
end
order = FactoryBot.create(:order)
order.customer.present? # => true
order.shipping_address.city # => "Springfield"
Advanced Usage
Traits extend factories with named attribute sets that modify base factory definitions. Define traits within factory blocks and apply them selectively to create object variations without duplicating factory definitions.
FactoryBot.define do
factory :article do
title { "Default Article" }
content { "Article content" }
status { :draft }
published_at { nil }
trait :published do
status { :published }
published_at { Time.current }
end
trait :featured do
featured { true }
featured_at { Time.current }
end
trait :with_comments do
after(:create) do |article|
create_list(:comment, 3, article: article)
end
end
end
end
# Apply single trait
article = FactoryBot.create(:article, :published)
# Apply multiple traits
article = FactoryBot.create(:article, :published, :featured)
# Combine traits with attribute overrides
article = FactoryBot.create(:article, :published, title: "Custom Title")
Nested factories inherit attributes from parent factories while allowing customization for specialized use cases. Child factories modify or extend parent definitions without duplicating common attributes.
FactoryBot.define do
factory :user do
name { "Regular User" }
email { generate(:email) }
role { :member }
factory :admin_user do
role { :admin }
permissions { [:read, :write, :delete] }
end
factory :guest_user do
name { "Guest User" }
role { :guest }
temporary { true }
end
end
end
admin = FactoryBot.create(:admin_user)
admin.role # => :admin
admin.permissions # => [:read, :write, :delete]
Callbacks execute custom logic at specific points during object creation. FactoryBot provides before
, after
, and around
hooks for each creation method, enabling complex setup and teardown operations.
FactoryBot.define do
factory :user_with_profile do
name { "User Name" }
email { generate(:email) }
after(:build) do |user|
user.profile = build(:profile, user: user)
end
after(:create) do |user|
create(:authentication_token, user: user)
user.send_welcome_email
end
end
factory :team do
name { "Development Team" }
after(:create) do |team, evaluator|
create_list(:user, evaluator.member_count, team: team)
end
# Custom transient attribute
transient do
member_count { 3 }
end
end
end
team = FactoryBot.create(:team, member_count: 5)
team.users.count # => 5
Transient attributes pass parameters to factories without adding them to the created objects. Use transient attributes for conditional logic, iteration counts, or configuration values that affect factory behavior.
FactoryBot.define do
factory :order_with_items do
customer_name { "Customer" }
transient do
item_count { 2 }
item_price { 10.00 }
end
after(:create) do |order, evaluator|
create_list(:order_item, evaluator.item_count,
order: order,
price: evaluator.item_price)
order.calculate_total
end
end
end
order = FactoryBot.create(:order_with_items,
item_count: 4,
item_price: 15.00)
order.items.count # => 4
order.total # => 60.00
Custom methods within factory definitions handle complex attribute generation logic. Define methods directly in factory blocks and reference them in attribute definitions.
FactoryBot.define do
factory :financial_report do
report_date { Date.current }
# Custom method for complex calculation
revenue { calculate_monthly_revenue }
expenses { calculate_monthly_expenses }
profit { revenue - expenses }
private
def calculate_monthly_revenue
base_amount = rand(10_000..50_000)
seasonal_modifier = report_date.month.in?([11, 12]) ? 1.2 : 1.0
(base_amount * seasonal_modifier).round(2)
end
def calculate_monthly_expenses
revenue * 0.7 + rand(1_000..5_000)
end
end
end
Testing Strategies
Factory organization strategies improve test maintainability and reduce duplication across test suites. Group related factories in separate files and establish naming conventions that reflect domain models and test scenarios.
# spec/factories/users.rb
FactoryBot.define do
factory :user do
name { Faker::Name.full_name }
email { Faker::Internet.email }
factory :premium_user do
subscription_tier { :premium }
subscription_expires_at { 1.year.from_now }
end
end
end
# spec/factories/orders.rb
FactoryBot.define do
factory :order do
association :user
status { :pending }
trait :completed do
status { :completed }
completed_at { Time.current }
end
trait :with_payment do
after(:create) do |order|
create(:payment, order: order, amount: order.total)
end
end
end
end
Test data isolation prevents test interdependencies by ensuring each test receives fresh factory-generated objects. Use database transactions or explicit cleanup to maintain test independence.
# RSpec configuration for factory isolation
RSpec.configure do |config|
config.use_transactional_fixtures = true
config.before(:each) do
FactoryBot.sequences.each(&:rewind)
end
end
# Explicit test isolation
RSpec.describe OrderProcessor do
let(:user) { create(:user) }
let(:product) { create(:product, price: 25.00) }
it "processes valid orders" do
order = create(:order, user: user)
create(:order_item, order: order, product: product, quantity: 2)
result = OrderProcessor.new(order).process
expect(result).to be_successful
expect(order.reload.status).to eq('completed')
end
end
Factory testing validates factory definitions themselves, ensuring they produce valid objects and handle edge cases correctly. Test factories in isolation to catch definition errors before they affect other tests.
# spec/factories_spec.rb
RSpec.describe "Factories" do
FactoryBot.factories.map(&:name).each do |factory_name|
describe "#{factory_name} factory" do
it "creates valid objects" do
object = build(factory_name)
expect(object).to be_valid
end
it "persists successfully" do
expect { create(factory_name) }.not_to raise_error
end
end
end
describe "factory traits" do
it "creates valid published articles" do
article = build(:article, :published)
expect(article).to be_valid
expect(article.status).to eq('published')
expect(article.published_at).to be_present
end
end
end
Mock and stub integration with factories handles external dependencies during test execution. Combine factories with mocking libraries to control external service interactions while maintaining realistic object structures.
RSpec.describe NotificationService do
let(:user) { create(:user, email: "test@example.com") }
it "sends welcome emails to new users" do
email_service = instance_double("EmailService")
allow(EmailService).to receive(:new).and_return(email_service)
allow(email_service).to receive(:deliver)
NotificationService.welcome_user(user)
expect(email_service).to have_received(:deliver).with(
to: "test@example.com",
template: "welcome",
data: hash_including(user_name: user.name)
)
end
end
Performance testing with factories requires careful consideration of object creation overhead. Use build_stubbed
for tests that don't require database persistence and implement factory caching for expensive object creation.
# Performance-optimized factory usage
RSpec.describe CalculationEngine do
# Use build_stubbed for objects that don't need persistence
let(:user) { build_stubbed(:user) }
let(:preferences) { build_stubbed(:user_preferences, user: user) }
it "calculates user scores efficiently" do
# Stubbed objects avoid database overhead
score = CalculationEngine.calculate_score(user, preferences)
expect(score).to be > 0
end
end
# Factory caching for expensive setups
RSpec.describe ReportGenerator do
# Cache expensive factory creation
let(:complex_dataset) do
Rails.cache.fetch("test_dataset_#{__FILE__}#{__LINE__}") do
create(:dataset_with_relationships, record_count: 1000)
end
end
after(:all) do
Rails.cache.clear
end
end
Common Pitfalls
Sequence collisions occur when multiple sequences or database auto-increments generate conflicting identifiers. This problem manifests in tests that assume specific ID values or when sequences don't account for existing database records.
# Problematic: Assumes specific sequence values
FactoryBot.define do
factory :user do
sequence(:external_id) { |n| n }
end
end
# Test assumes external_id will be 1
user = create(:user)
expect(user.external_id).to eq(1) # May fail if sequence was used elsewhere
# Better: Don't assume sequence values
user = create(:user)
expect(user.external_id).to be > 0
expect(user.external_id).to be_a(Integer)
# Reset sequences between test runs
RSpec.configure do |config|
config.before(:suite) do
FactoryBot.sequences.each(&:rewind)
end
end
Association overbuilding creates unnecessary database records when tests only need partial object graphs. FactoryBot creates all associated objects by default, leading to performance degradation and test data pollution.
# Problematic: Creates unnecessary associated objects
factory :order_with_everything do
association :customer # Creates user + profile + preferences
association :shipping_address
association :billing_address
after(:create) do |order|
create_list(:order_item, 5, order: order) # Each creates product + category
end
end
# Better: Use build_stubbed for non-critical associations
factory :order do
association :customer, strategy: :build_stubbed
total { 100.00 }
trait :with_items do
after(:build) do |order|
# Build items without persisting associated products
order.items = build_list(:order_item, 3, order: order)
end
end
end
Callback ordering issues arise when multiple callbacks or traits modify the same attributes in conflicting ways. FactoryBot executes callbacks in definition order, which may not match logical dependencies.
# Problematic: Callback order affects final state
factory :article do
title { "Default Title" }
trait :published do
after(:create) do |article|
article.update!(status: :published, published_at: Time.current)
end
end
trait :featured do
after(:create) do |article|
article.update!(featured: true)
# This runs after published trait, may reset published_at
article.update!(published_at: Time.current) if article.draft?
end
end
end
# Better: Use single callback with conditional logic
trait :published do
status { :published }
published_at { Time.current }
end
trait :featured do
featured { true }
after(:build) do |article|
if article.draft?
article.status = :published
article.published_at = Time.current
end
end
end
Factory definition leakage occurs when factory modifications in one test file affect factories used in other tests. This happens with global factory modifications or sequence state that persists between tests.
# Problematic: Global factory modification
# In one test file
FactoryBot.modify do
factory :user do
role { :admin } # Changes factory globally
end
end
# In another test file - user factory now creates admins by default
user = create(:user)
user.role # => :admin (unexpected)
# Better: Use local overrides or traits
# Test-specific customization
let(:admin_user) { create(:user, role: :admin) }
# Or define specific factories
factory :admin_user, parent: :user do
role { :admin }
end
Memory leak patterns develop when factories create large object graphs that aren't properly cleaned up between tests. This is common with callbacks that create additional objects or establish circular references.
# Problematic: Creates circular references
factory :user_with_full_profile do
name { "User" }
after(:create) do |user|
profile = create(:profile, user: user)
# Creates bidirectional references that may not be GC'd properly
user.profile = profile
profile.user = user
# Creates many objects that accumulate
create_list(:activity_log, 50, user: user)
end
end
# Better: Use minimal object creation and explicit cleanup
factory :user do
name { "User" }
trait :with_profile do
after(:create) do |user|
# Single direction reference
create(:profile, user: user)
end
end
trait :with_recent_activity do
after(:create) do |user|
# Create minimal test data
create_list(:activity_log, 3, user: user)
end
end
end
# Explicit cleanup in tests
after(:each) do
# Clear any cached factory objects
FactoryBot.sequences.each(&:rewind)
end
Reference
Core Factory Methods
Method | Parameters | Returns | Description |
---|---|---|---|
FactoryBot.create(name, **attrs) |
Factory name (Symbol), attribute overrides (Hash) | Object |
Creates and persists object to database |
FactoryBot.build(name, **attrs) |
Factory name (Symbol), attribute overrides (Hash) | Object |
Builds object in memory without persistence |
FactoryBot.build_stubbed(name, **attrs) |
Factory name (Symbol), attribute overrides (Hash) | Object |
Builds object with stubbed associations and ID |
FactoryBot.attributes_for(name, **attrs) |
Factory name (Symbol), attribute overrides (Hash) | Hash |
Returns hash of attributes without creating object |
FactoryBot.create_list(name, count, **attrs) |
Factory name (Symbol), count (Integer), overrides (Hash) | Array |
Creates array of persisted objects |
FactoryBot.build_list(name, count, **attrs) |
Factory name (Symbol), count (Integer), overrides (Hash) | Array |
Builds array of objects without persistence |
Factory Definition Syntax
Element | Syntax | Description |
---|---|---|
Basic Factory | factory :name do |
Defines factory with name matching class |
Custom Class | factory :name, class: 'ClassName' do |
Specifies different class than factory name |
Parent Factory | factory :child, parent: :parent do |
Inherits attributes from parent factory |
Trait Definition | trait :trait_name do |
Defines reusable attribute modification |
Association | association :relation |
Creates associated object using factory |
Sequence | `sequence(:attr) { | n |
Transient Attribute | transient do; attr { value }; end |
Defines parameter not added to object |
Callback Methods
Callback | Timing | Usage |
---|---|---|
before(:build) |
Before object initialization | Set up prerequisites for object creation |
after(:build) |
After object built in memory | Modify object before persistence |
before(:create) |
Before database persistence | Final modifications before saving |
after(:create) |
After database persistence | Create associated objects or trigger external actions |
around(:build) |
Wraps build process | Custom build logic with yield control |
around(:create) |
Wraps create process | Custom persistence logic with yield control |
Association Options
Option | Values | Description |
---|---|---|
factory |
Symbol | Specifies factory name for association |
strategy |
:build , :create , :build_stubbed |
Controls how association is created |
foreign_key |
String/Symbol | Specifies custom foreign key attribute |
traits |
Array of Symbols | Applies traits to associated object |
Sequence Configuration
# Global sequence definition
FactoryBot.define do
sequence :email do |n|
"user#{n}@example.com"
end
sequence :order_number, 1000 do |n|
"ORD-#{sprintf('%04d', n)}"
end
sequence :uuid do
SecureRandom.uuid
end
end
Configuration Options
Setting | Default | Description |
---|---|---|
FactoryBot.use_parent_strategy |
true |
Inherit build strategy from parent objects |
FactoryBot.allow_class_lookup |
true |
Automatically determine class from factory name |
FactoryBot.duplicate_attribute_assignment_from_initialize_with |
true |
Handle duplicate attributes in custom initialization |
Common Error Types
Error | Cause | Solution |
---|---|---|
FactoryBot::DuplicateDefinitionError |
Factory or sequence defined multiple times | Use unique names or FactoryBot.modify |
FactoryBot::AssociationDefinitionError |
Invalid association configuration | Check factory name and association options |
FactoryBot::AttributeDefinitionError |
Attribute conflicts with reserved names | Rename attribute or use custom initialization |
ActiveRecord::RecordInvalid |
Factory creates invalid objects | Add validation checks to factory definition |
Integration Patterns
# Rails model integration
class User < ApplicationRecord
validates :email, presence: true, uniqueness: true
has_many :orders
enum role: { member: 0, admin: 1, guest: 2 }
end
# Corresponding factory
FactoryBot.define do
factory :user do
sequence(:email) { |n| "user#{n}@example.com" }
name { Faker::Name.full_name }
role { :member }
trait :admin do
role { :admin }
end
factory :user_with_orders do
after(:create) do |user|
create_list(:order, 3, user: user)
end
end
end
end
Performance Guidelines
Scenario | Recommendation | Rationale |
---|---|---|
Unit tests | Use build or build_stubbed |
Avoid database overhead |
Integration tests | Use create selectively |
Only persist objects requiring database state |
Large object graphs | Create minimal associations | Reduce object creation time |
Repeated test data | Cache expensive factories | Amortize creation cost across tests |
Sequence-heavy tests | Reset sequences between runs | Ensure predictable test behavior |