Low commit activity in last 3 years
Lightweight DSL for building test data objects without ActiveRecord. Define factories with default attributes, apply traits for variations, and use thread-safe sequences for unique values.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies
 Project Readme

philiprehberger-test_factory

Tests Gem Version Last updated

Lightweight test data factory DSL with sequences and traits

Requirements

  • Ruby >= 3.1

Installation

Add to your Gemfile:

gem "philiprehberger-test_factory"

Or install directly:

gem install philiprehberger-test_factory

Usage

require "philiprehberger/test_factory"

# Define a factory
Philiprehberger::TestFactory.define(:user) do
  { name: "Alice", email: "alice@example.com", role: "user" }
end

# Build a single object
user = Philiprehberger::TestFactory.build(:user)
# => { name: "Alice", email: "alice@example.com", role: "user" }

# Build with overrides
admin = Philiprehberger::TestFactory.build(:user, role: "admin")
# => { name: "Alice", email: "alice@example.com", role: "admin" }

Traits

Philiprehberger::TestFactory.trait(:user, :admin) { { role: "admin" } }
Philiprehberger::TestFactory.trait(:user, :inactive) { { active: false } }

admin = Philiprehberger::TestFactory.build(:user, traits: [:admin])
# => { name: "Alice", email: "alice@example.com", role: "admin" }

Sequences

Philiprehberger::TestFactory.sequence(:email) { |n| "user_#{n}@example.com" }

# Access via the registry
email = Philiprehberger::TestFactory.send(:registry).next_in_sequence(:email)
# => "user_1@example.com"

Collections

Generate N objects at once with build_list. Each element goes through the normal build path, so sequences increment once per object and overrides or traits apply identically to every element.

Philiprehberger::TestFactory.sequence(:email) { |n| "user_#{n}@example.com" }
Philiprehberger::TestFactory.define(:user) do
  {
    name: "User",
    email: Philiprehberger::TestFactory.send(:registry).next_in_sequence(:email),
    role: "user"
  }
end

users = Philiprehberger::TestFactory.build_list(:user, 3)
# => [
#      { name: "User", email: "user_1@example.com", role: "user" },
#      { name: "User", email: "user_2@example.com", role: "user" },
#      { name: "User", email: "user_3@example.com", role: "user" }
#    ]

# Apply overrides and traits to every element
Philiprehberger::TestFactory.trait(:user, :admin) { { role: "admin" } }

admins = Philiprehberger::TestFactory.build_list(:user, 2, traits: [:admin], name: "Bob")
# => [
#      { name: "Bob", email: "user_4@example.com", role: "admin" },
#      { name: "Bob", email: "user_5@example.com", role: "admin" }
#    ]

# count == 0 returns an empty array
Philiprehberger::TestFactory.build_list(:user, 0)
# => []

# Negative counts raise ArgumentError
Philiprehberger::TestFactory.build_list(:user, -1)
# => ArgumentError: count must be non-negative, got -1

# Fixed-size helpers:
Philiprehberger::TestFactory.build_pair(:user)  # exactly 2
Philiprehberger::TestFactory.build_trio(:user)  # exactly 3

Callbacks

Philiprehberger::TestFactory.define(:user) do |f|
  f.after_build { |obj| obj[:created_at] = Time.now }
  { name: "Alice", email: "alice@example.com" }
end

Transient Attributes

Philiprehberger::TestFactory.define(:user) do |f|
  f.transient { admin false }
  f.after_build { |obj, transients| obj[:role] = "admin" if transients[:admin] }
  { name: "Alice", role: "user" }
end

user = Philiprehberger::TestFactory.build(:user, admin: true)
# => { name: "Alice", role: "admin" }

Associations

Philiprehberger::TestFactory.define(:user) { { name: "Alice" } }
Philiprehberger::TestFactory.define(:post) do |f|
  f.association :author, factory: :user
  { title: "Hello World" }
end

post = Philiprehberger::TestFactory.build(:post)
# => { title: "Hello World", author: { name: "Alice" } }

Attributes For

Return a plain attribute hash without firing after_build callbacks or resolving association objects — useful for unit tests that need the raw attributes without nested dependencies. Mirrors FactoryBot's attributes_for.

Philiprehberger::TestFactory.define(:user) do
  { name: "Alice", role: "user" }
end

attrs = Philiprehberger::TestFactory.attributes_for(:user, role: :admin)
# => { name: "Alice", role: :admin }

Reset

Philiprehberger::TestFactory.reset!

Registry Inspection

Check which factories are registered without poking at internal state. Useful in shared spec helpers or library setup that should only register a factory when it isn't already defined.

Philiprehberger::TestFactory.define(:user) { { name: "Alice" } }

Philiprehberger::TestFactory.factories
# => [:user]

Philiprehberger::TestFactory.defined?(:user)
# => true

Philiprehberger::TestFactory.defined?(:post)
# => false

API

Method Description
TestFactory.define(name, &block) Register a factory; block returns a hash of defaults
TestFactory.trait(factory_name, trait_name, &block) Register a trait override for a factory
TestFactory.sequence(name, &block) Register a thread-safe auto-incrementing sequence
TestFactory.build(name, traits:, **overrides) Build a single data hash
TestFactory.build_list(name, count, traits:, **overrides) Build N data hashes
TestFactory.build_pair(name, traits:, **overrides) Build exactly 2 data hashes (FactoryBot-compatible convenience)
TestFactory.build_trio(name, traits:, **overrides) Build exactly 3 data hashes
TestFactory.attributes_for(name, traits:, **overrides) Resolve attributes without after_build callbacks or associations
TestFactory.reset! Clear all definitions, traits, and sequences
TestFactory.factories List all registered factory names in registration order
TestFactory.defined?(name) Whether a factory has been registered under the given name
DefinitionProxy#after_build(&block) Register a callback that runs after building
DefinitionProxy#transient(&block) Declare transient attributes excluded from the result
DefinitionProxy#association(name, factory:) Declare an association to another factory

Development

bundle install
bundle exec rspec
bundle exec rubocop

Support

If you find this project useful:

Star the repo

🐛 Report issues

💡 Suggest features

❤️ Sponsor development

🌐 All Open Source Projects

💻 GitHub Profile

🔗 LinkedIn Profile

License

MIT