Skip to main content
Skip table of contents

FactoryBot testing guidelines

Last Updated: July 9, 2025

FactoryBot is a library for building test data objects in Ruby applications. This guide covers best practices for creating maintainable, reliable factories and using them effectively in your test suite.

Philosophy

Factories should be pure data builders that create valid objects with sensible defaults. They shouldn't contain business logic, external service calls, or test setup code. This separation keeps factories reusable across different test scenarios.

Basic usage

Creating factories

RUBY
# spec/factories/users.rb
FactoryBot.define do
  factory :user do
    first_name { 'John' }
    last_name { 'Doe' }
    sequence(:email) { |n| "user#{n}@example.com" }
    sequence(:ssn) { |n| "123-45-#{n.to_s.rjust(4, '0')}" }
  end
end

Using factories in tests

In spec files, you can use these methods to create objects:

RUBY
# In spec files
user = create(:user)                    # Persisted object
user = build(:user)                     # Non-persisted object
user = build_stubbed(:user)             # Stubbed object (faster)
user = create(:user, first_name: 'Jane') # Override attributes

Method availability

  • In spec files: Use shorthand build(:user) and create(:user)

  • In support files: Use explicit FactoryBot.build(:user) and FactoryBot.create(:user)

These shorthands work in any file where you include FactoryBot::Syntax::Methods. Many Vets-API modules enable it globally.

Advanced usage

Traits

Use traits to create variations of a factory:

RUBY
factory :user do
  first_name { 'John' }
  last_name { 'Doe' }
  
  trait :admin do
    admin { true }
  end
  
  trait :with_benefits do
    benefits_status { 'active' }
  end
end

Then use traits in your tests:

RUBY
admin_user = create(:user, :admin)
user_with_benefits = create(:user, :with_benefits)

Associations

Handle relationships between objects:

RUBY
factory :claim do
  user
  status { 'pending' }
  
  # Explicit association
  factory :claim_with_user do
    association :user, factory: :user, first_name: 'Jane'
  end
end

Sequences

Generate unique values:

RUBY
factory :user do
  sequence(:email) { |n| "user#{n}@example.com" }
  sequence(:ssn) { |n| "123-45-#{n.to_s.rjust(4, '0')}" }
end

Callbacks

Use callbacks sparingly and only for data setup:

RUBY
factory :user do
  first_name { 'John' }
  
  after(:build) do |user|
    user.full_name = "#{user.first_name} #{user.last_name}"
  end
end

Additional methods

attributes_for

Get a hash of attributes without creating an object:

RUBY
attrs = attributes_for(:user)
# => { first_name: 'John', last_name: 'Doe', email: 'user1@example.com' }

create_list and build_list

Create multiple objects at once:

RUBY
users = create_list(:user, 3)
claims = build_list(:claim, 5, user: user)

transient attributes

Use temporary attributes that won't be passed to the model:

RUBY
factory :user do
  transient do
    claim_count { 2 }
  end
  
  first_name { 'John' }
  
  after(:create) do |user, evaluator|
    create_list(:claim, evaluator.claim_count, user: user)
  end
end

Best practices

1. Keep factories simple

  • Use sensible defaults that work for most tests

  • Avoid complex calculations or external dependencies

  • Focus on creating valid, minimal objects

Here's how to create simple, effective factories:

RUBY
# Good
factory :user do
  first_name { 'John' }
  last_name { 'Doe' }
  sequence(:email) { |n| "user#{n}@example.com" }
end

# Avoid
factory :user do
  first_name { calculate_random_name_from_api }
  complex_field { perform_expensive_operation }
end

2. Use traits for variations

Instead of creating many similar factories, use traits:

RUBY
# Good
factory :user do
  first_name { 'John' }
  
  trait :admin do
    role { 'admin' }
  end
  
  trait :veteran do
    military_service { true }
  end
end

# Less ideal
factory :admin_user do
  first_name { 'John' }
  role { 'admin' }
end

factory :veteran_user do
  first_name { 'John' }
  military_service { true }
end

3. Separate test setup from data creation

Keep stubbing and mocking in your test files, not in factories:

RUBY
# In your spec file
describe UserService do
  let(:user) { build(:user) }
  
  before do
    stub_mpi(user.icn)
    allow(ExternalService).to receive(:call)
  end
end

4. Use build_stubbed for performance

When you don't need database persistence:

RUBY
# Faster - no database interaction
user = build_stubbed(:user)

# Slower - saves to database
user = create(:user)

Objects returned by build_stubbed are read-only—calling save or update raises RuntimeError.

5. Override attributes explicitly

Make test intentions clear by overriding attributes:

RUBY
# Clear intent
user = create(:user, first_name: 'Jane', admin: true)

# Less clear
user = create(:admin_user_named_jane)

6. Lint your factories

Add FactoryBot.lint in a before-suite hook to catch invalid factories early:

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

Common pitfalls

1. RSpec helpers aren't available inside factory blocks

Problem: Custom test helpers, stubs, and RSpec mock methods aren't available inside factory definitions.

This will fail:

RUBY
factory :user do
  after(:build) do |user|
    stub_mpi(user.icn) # NoMethodError
  end
end

Solution:

RUBY
# Factory (clean data only)
factory :user do
  first_name { 'John' }
  icn { '123456789' }
end

# In spec file
let(:user) { build(:user) }
before { stub_mpi(user.icn) }

2. Method availability outside RSpec

Problem: build() and create() shorthand methods aren't available in support files.

This will fail:

RUBY
# spec/support/test_helper.rb
def create_test_user
  create(:user) # NoMethodError
end

Solution:

RUBY
# spec/support/test_helper.rb
def create_test_user
  FactoryBot.create(:user)
end

3. Overly complex factories

Problem: Factories that do too much become hard to maintain and debug.

Avoid this:

RUBY
factory :user do
  after(:create) do |user|
    create_list(:claim, 5, user: user)
    user.update!(status: calculate_complex_status)
    send_notification_email(user)
  end
end

Better approach:

RUBY
factory :user do
  first_name { 'John' }
  status { 'active' }
end

# Create associations in tests when needed
let(:user) { create(:user) }
let(:claims) { create_list(:claim, 5, user: user) }

Troubleshooting

Common errors

Error: NoMethodError: undefined method 'stub_mpi'
Solution: Move stubbing from factory definitions to RSpec before blocks

Error: NoMethodError: undefined method 'build'
Solution: Use FactoryBot.build in non-spec files

Error: ArgumentError: Factory not registered
Solution: Check that factories are properly defined in spec/factories/

Debugging tips

  1. Run individual specs to isolate factory issues

  2. Check factory definitions in spec/factories/ directory

  3. Verify method availability based on file context (spec vs support)

  4. Test factory creation in isolation: FactoryBot.create(:user)

Summary

FactoryBot factories should be simple, focused data builders. Keep test setup, stubbing, and complex logic in your spec files where they belong. This separation leads to more maintainable tests and reusable factories.

Key principles:

  • Factories create data, specs handle test logic

  • Use explicit FactoryBot.method syntax outside spec files

  • Keep factories simple and focused

  • Use traits for variations, not separate factories


JavaScript errors detected

Please note, these errors can depend on your browser setup.

If this problem persists, please contact our support.