ActionFactory
A lightweight, object-oriented factory library for Ruby and Rails that centralizes record creation with minimal overhead.
Philosophy
ActionFactory is designed to be a lightweight alternative to more feature-rich factory libraries. It's built for teams that:
- Use fixtures as their primary testing strategy but need factories for specific use cases
- Want centralized record creation without the overhead of replacing their entire test data strategy
- Prefer object-oriented design with explicit factory classes over DSLs
- Need ActiveRecord integration but want to keep it optional and modular
Unlike other factory libraries, ActionFactory doesn't try to replace your entire test data strategy. Instead, it complements fixture-based workflows by providing a clean way to create records when fixtures aren't the right tool for the job.
Key Features
- Object-oriented factory classes with clear inheritance
- Lightweight with minimal dependencies
- Optional ActiveRecord integration with association helpers
- Flexible strategies (build vs create)
- Traits for composable variations
- Sequences for unique values
- Lifecycle callbacks using ActiveModel::Callbacks
Installation
Via bundler:
bundle add action_factoryOr install it yourself:
gem install action_factoryUsage
Setup
Create a base factory class:
class ApplicationFactory < ActionFactory::Base
endIf you're using ActiveRecord, add the association helpers:
class ApplicationFactory < ActionFactory::Base
include ActionFactory::ActiveRecord
endCreating Your First Factory
class UserFactory < ApplicationFactory
attribute(:name) { "Hank Green" }
attribute(:email_prefix) { name.downcase.gsub(" ", ".") }
sequence(:email) { |i| "#{email_prefix}#{i}@example.com" }
attribute(:role) { "user" }
trait(:admin) do
instance.role = "admin"
end
trait(:activated) do
instance.activate!
end
endUsing Factories
ActionFactory provides three ways to create instances, from most convenient to most explicit:
1. Module-Level Methods (Recommended)
# Build an instance (doesn't save to database)
user = ActionFactory.build(:user)
# => #<User name="Hank Green", email="hank.green1@example.com", role="user">
# Create an instance (saves to database)
user = ActionFactory.create(:user)
# => #<User id=1, name="Hank Green", email="hank.green2@example.com", role="user">
# With attributes
user = ActionFactory.create(:user, name: "Jane Doe")
# => #<User id=2, name="Jane Doe", email="jane.doe3@example.com", role="user">
# With traits
admin = ActionFactory.create(:user, :admin)
# => #<User id=3, name="Hank Green", email="hank.green4@example.com", role="admin">
# With traits and attributes
admin = ActionFactory.create(:user, :admin, :activated, name: "Jane Doe")
# => #<User id=4, name="Jane Doe", email="jane.doe5@example.com", role="admin">2. Factory Class Methods
# Build an instance
user = UserFactory.build
# => #<User name="Hank Green", email="hank.green1@example.com", role="user">
# Create an instance
user = UserFactory.create(name: "Jane Doe")
# => #<User id=1, name="Jane Doe", email="jane.doe2@example.com", role="user">
# With traits
admin = UserFactory.create(:admin, :activated)
# => #<User id=2, name="Hank Green", email="hank.green3@example.com", role="admin">3. Factory Instance (Most Explicit)
# Useful when you need to inspect factory state or customize behavior
factory = UserFactory.new(:admin, name: "Jane Doe")
factory.traits # => [:admin]
factory.attributes # => {:name=>"Jane Doe"}
user = factory.create # => #<User id=1, name="Jane Doe", role="admin">Using Helpers (Optional)
For even shorter syntax in tests, include ActionFactory::Helpers:
RSpec.configure do |config|
config.include ActionFactory::Helpers
end
# In your tests - just like using ActionFactory.build/create
user = build(:user) # Equivalent to ActionFactory.build(:user)
user = create(:user, name: "John Doe")
user = create(:user, :admin, :activated)Note: The helper methods are convenience wrappers. We recommend using ActionFactory.build and ActionFactory.create directly for better clarity about where the methods come from.
Getting the Factory Class
If you need access to the factory class itself:
factory_class = ActionFactory.factory_for(:user)
# => UserFactory
# Useful for metaprogramming or inspecting factory configuration
factory_class.assignments
factory_class.klass # => UserSequences
Sequences generate unique values each time they're called:
class UserFactory < ApplicationFactory
sequence(:email) { |i| "user#{i}@example.com" }
end
user1 = ActionFactory.create(:user) # => email: "user1@example.com"
user2 = ActionFactory.create(:user) # => email: "user2@example.com"You can reference other attributes in sequences:
class UserFactory < ApplicationFactory
attribute(:username) { "jdoe" }
sequence(:email) { |i| "#{username}#{i}@example.com" }
end
user = ActionFactory.create(:user, username: "janedoe") # => email: "janedoe1@example.com"Traits
Traits allow you to define variations of your factory:
class PostFactory < ApplicationFactory
attribute(:title) { "My Post" }
attribute(:status) { "draft" }
trait(:published) do
instance.status = "published"
instance.published_at = Time.current
end
trait(:with_long_title) do
instance.title = "This is a very long title that exceeds normal length"
end
end
post = ActionFactory.create(:post, :published, :with_long_title)Associations (ActiveRecord)
When you include ActionFactory::ActiveRecord, you get powerful association helpers:
class PostFactory < ApplicationFactory
include ActionFactory::ActiveRecord
attribute(:title) { "My Post" }
attribute(:body) { "Post content here" }
# Automatically builds/creates associated records
association :author, factory: :user, strategy: :create
end
# When you create a post, it automatically creates an author
post = ActionFactory.create(:post)
post.author # => #<User id=1, ...>Association Options
Without a block - Uses the specified strategy or inherits from parent:
# Always create the author, even when building the post
association :author, factory: :user, strategy: :create
# Use the same strategy as the parent (build post = build author, create post = create author)
association :author, factory: :user
# Apply traits to the associated record
association :author, factory: :user, traits: [:admin, :activated]With a block - Full control over association creation:
# The block receives (runner, strategy) where strategy is :build or :create
association :author do |runner, strategy|
# Option 1: Use the parent's strategy
runner.run(strategy)
end
association :author do |runner, strategy|
# Option 2: Always create, regardless of parent strategy
runner.run(:create)
end
association :author do |runner, strategy|
# Option 3: Conditional logic
if strategy == :create
runner.run(:create, :verified) # Created posts get verified authors
else
runner.run(:build)
end
end
association :author do |runner, strategy|
# Option 4: Custom logic, bypass the factory entirely
User.find_by(email: "default@example.com") || runner.run(strategy)
endAssociation Inheritance
Associations can be overridden in subclasses:
class PostFactory < ApplicationFactory
association :author, factory: :user
end
class PublishedPostFactory < PostFactory
# Override to always use an admin
association :author, factory: :user, traits: [:admin]
endCallbacks
ActionFactory uses ActiveModel::Callbacks to provide lifecycle hooks:
after_initialize # After the instance is initialized
before_assign_attributes # Before attributes are assigned
around_assign_attributes # Around attribute assignment
after_assign_attributes # After attributes are assigned
before_create # Before saving to database
around_create # Around saving to database
after_create # After saving to databaseExample usage:
class MyModelFactory < ApplicationFactory
after_initialize :set_defaults
before_create :prepare_for_save
after_create :send_notification
private
def set_defaults
instance.some_attribute = true if attributes[:some_attribute].blank?
end
def prepare_for_save
instance.normalize_data
end
def send_notification
NotificationService.send(instance)
end
endCustom Initialization and Persistence
You can customize how instances are initialized and persisted:
class UserFactory < ApplicationFactory
# Custom initialization
to_initialize do |factory|
User.new.tap do |user|
user.created_by_factory = true
end
end
# Custom persistence
to_create do |factory|
instance.save(validate: false)
instance.reload
end
endOr configure globally:
ActionFactory.configure do |config|
config.initialize_method = :new # default
config.persist_method = :save! # default
endFactory Registration
By default, ActionFactory infers the model class from the factory name:
class UserFactory < ApplicationFactory
end
# => Creates User instancesFor custom mappings, use register:
class AdminFactory < ApplicationFactory
register class_name: "User"
end
# => Creates User instances
class SpecialUserFactory < ApplicationFactory
register factory_name: :special, class_name: "User"
end
# => Available as create(:special), creates User instancesInheritance
Factories support inheritance, making it easy to create variations:
class UserFactory < ApplicationFactory
attribute(:name) { "John Doe" }
attribute(:role) { "user" }
end
class AdminFactory < UserFactory
attribute(:role) { "admin" }
attribute(:permissions) { "all" }
end
admin = ActionFactory.create(:admin)
# => #<User name="John Doe", role="admin", permissions="all">Patterns and Best Practices
Fixture-First, Factory-Second
Use fixtures for your standard test data and factories for:
- Tests that need many variations of the same record
- Tests that require specific attribute combinations
- Integration tests that need to create records with associations
- Tests that need to create records with state that's hard to represent in fixtures
# test/fixtures/users.yml
admin:
name: "Admin User"
email: "admin@example.com"
role: "admin"
# Use fixtures for standard cases
test "admin can access dashboard" do
user = users(:admin)
assert user.can_access_dashboard?
end
# Use factories for variations
test "user with specific permissions" do
user = ActionFactory.create(:user, permissions: {read: true, write: false, delete: false})
assert_not user.can_delete?
endKeep Factories Simple
Factories should create valid, minimal records:
# Good - minimal and clear
class UserFactory < ApplicationFactory
attribute(:email) { "user@example.com" }
attribute(:name) { "Test User" }
end
# Avoid - too much setup logic
class UserFactory < ApplicationFactory
attribute(:email) { "user@example.com" }
attribute(:name) { "Test User" }
after_create :create_profile
after_create :send_welcome_email
after_create :add_to_mailing_list
after_create :create_default_preferences
endUse Traits for Variations
class UserFactory < ApplicationFactory
attribute(:status) { "pending" }
trait(:active) do
instance.status = "active"
instance.activated_at = Time.current
end
trait(:suspended) do
instance.status = "suspended"
instance.suspended_at = Time.current
end
endAssociation Strategies
Choose the right strategy for your associations:
class PostFactory < ApplicationFactory
# Build for unit tests (faster, no database hits for associations)
association :author, factory: :user, strategy: :build
# Create for integration tests (proper foreign keys and database constraints)
association :author, factory: :user, strategy: :create
# Conditional for flexibility
association :author do |runner, strategy|
# Use the same strategy as the parent
runner.run(strategy)
end
endConfiguration
ActionFactory.configure do |config|
# Method called to initialize new instances (default: :new)
config.initialize_method = :new
# Method called to persist instances (default: :save!)
config.persist_method = :save!
endComparison with Other Libraries
vs FactoryBot: ActionFactory is lighter weight and designed to complement fixtures rather than replace them. It uses explicit factory classes instead of a DSL, making it easier to use object-oriented patterns like inheritance and composition.
vs Fabrication: Similar philosophy of lightweight factories, but ActionFactory has built-in ActiveRecord association support and uses a more traditional Ruby class structure.
vs Fixtures: ActionFactory generates fresh data for each test, avoiding issues with shared mutable state. Use factories when you need variations or complex setups that are hard to express in static YAML files.
Contributing
Contributions are welcome! Please feel free to submit issues and pull requests.
License
The gem is available as open source under the terms of the MIT License.