CrackedRuby logo

CrackedRuby

Test Factories

A comprehensive guide to creating and managing test data using factory patterns and libraries in Ruby testing environments.

Testing and Quality Test Patterns
8.2.5

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