FactoryMan
Elixir test data factories with automatic struct building, database insertion, and customizable hooks.
NOTE: FactoryMan is heavily inspired by ExMachina, and copies some code from it in some cases (e.g. for sequences). However, FactoryMan is not a clone of ExMachina. See the examples in the documentation to see what sets us apart!
Features
- Easy factories - Define factories with minimal boilerplate
- Generated functions - Build params (for testing changesets), structs, and lists of items
-
Database insertion - Built-in
insert_functions with configurable repo -
List factories - Create multiple records with
*_listfunctions - Sequence generation - Automatic unique value generation for usernames, emails, etc.
- Lazy evaluation - Compute values at build time
- Factory inheritance - Share config from parent factories to reduce boilerplate
- Hooks - Apply custom transformations to data before/after building or inserting a factory item
Installation
Add FactoryMan to your mix.exs dependencies:
def deps do
[
{:factory_man, "0.2.0"}
]
end
Then run mix deps.get.
No further configuration should be necessary.
Quick Start
Create a factory module in your project’s test/support/ directory:
test/support/factory.ex
defmodule MyApp.Factory do
use FactoryMan, repo: MyApp.Repo
alias MyApp.Users.User
deffactory user(params \\ %{}), struct: User do
base_params = %{username: "user-#{System.os_time()}"}
Map.merge(base_params, params)
end
endBuild and insert some test data:
# Build params (e.g. for testing changesets)
iex> MyApp.Factory.build_user_params(%{username: "test_user"})
%{username: "test_user"}
# Build a struct (not persisted)
iex> MyApp.Factory.build_user_struct(%{username: "test_user"})
%User{id: nil, username: "test_user"}
# Insert into the database
iex> MyApp.Factory.insert_user!(%{username: "test_user"})
%User{id: 1, username: "test_user"}
# Insert multiple items in a single statement
iex> MyApp.Factory.insert_user_list!(3)
[%User{id: 1, ...}, %User{id: 2, ...}, %User{id: 3, ...}]Base Factory Pattern (Optional)
For larger projects, you may want to share common configuration across multiple factory modules. FactoryMan allows you to create a base factory module with shared settings:
defmodule MyApp.Factory do
# Define the base factory options here
use FactoryMan, repo: MyApp.Repo
# You may define generic factory helpers in this module as well
def generate_username, do: "user-#{System.os_time()}"
endThen extend the base factory in child factory modules:
defmodule MyApp.Factory.ChildFactory do
# Extend the base factory
use FactoryMan, extends: MyApp.Factory
alias MyApp.Factory
alias MyApp.Users.User
# Child factories in this module inherit the options set in the base factory module
deffactory user(params \\ %{}), struct: User do
%{username: Factory.generate_username()} |> Map.merge(params)
end
end
Child factories are typically placed in test/support/factory/[your_context].ex and extend the
base factory to inherit common configuration like repo settings and hooks.
The directory structure is up to you, but it is recommended to make a factory module for each context in your application code. Keeping the filesystem hierarchies the same tends to make it easier to remember which factory is where.
Tip
The base factory pattern is completely optional. Use whatever structure fits your project or personal tastes.
Recommended Project Structure
Note
This is an opinionated recommendation, not a requirement. FactoryMan is intended to work with any module structure. However, having a consistent convention reduces decision fatigue and makes it easier to navigate factory code across projects.
We recommend keeping the base factory as a dedicated module focused on shared configuration: repo settings, hooks, and generic helpers.
# test/support/factory.ex
defmodule MyApp.Factory do
use FactoryMan, repo: MyApp.Repo
# Shared hooks
def after_insert_handler(%_{} = struct),
do: Ecto.reset_fields(struct, struct.__struct__.__schema__(:associations))
# Generic helpers used across multiple child factories
def generate_email(name), do: "#{name}@example.com"
endChild factory modules extend the base and mirror your application’s context structure:
test/support/
factory.ex # Base factory (config, hooks, shared helpers)
factory/
accounts.ex # MyApp.Factory.Accounts (extends MyApp.Factory)
blog.ex # MyApp.Factory.Blog (extends MyApp.Factory)
blog/comments.ex # MyApp.Factory.Blog.Comments (extends MyApp.Factory)test/support/factory/accounts.ex
defmodule MyApp.Factory.Accounts do
use FactoryMan, extends: MyApp.Factory
alias MyApp.Accounts.User
alias MyApp.Factory
deffactory user(params \\ %{}), struct: User do
base_params = %{
username: "user-#{System.os_time()}",
email: Factory.generate_email("user-#{System.os_time()}")
}
Map.merge(base_params, params)
end
endWhy this structure?
- The base factory is a clear, single-purpose module. It is easy to find, and easy to understand
- Child factories map to application contexts, so you always know where to look
-
The
extends:relationship is obvious and consistent - New team members (and LLM agents) can follow the pattern without guessing
When to Use Params vs Struct vs Insert
build_*_params- For testing changesets, passing to functions that expect maps, or when no struct shape is needed.build_*_struct- For setting association fields on other structs being built in memory. Use this when the record does not need to exist in the database at the time the struct is generated.insert_*!- When a foreign key constraint requires the record to exist in the database, or when the test itself queries the database for this record.Lazy evaluation (0-arity) - When a default value is expensive to compute or depends on runtime state. This avoids eagerly inserting records that may not be needed.
Lazy evaluation (1-arity) - When a field’s default depends on another field in the same factory. The function receives the parent map at build time.
A common mistake when building factories is inserting records into the database when a plain struct would suffice. If the only reason to insert is to get an ID for a foreign key, consider whether the test actually needs that constraint enforced. If not, a struct with a generated ID is simpler and faster.
Direct Struct Factories (params?: false)
For complex factories that need full control over struct construction, set params?: false.
The factory body returns a struct directly, and no build_*_params functions are generated:
deffactory invoice(params \\ %{}), struct: Invoice, params?: false do
customer =
case params[:customer] do
%Customer{} = customer -> customer
_ -> MyApp.Factory.Accounts.insert_customer!()
end
%Invoice{
customer: customer,
total: Map.get(params, :total, Enum.random(100..10_000))
}
end
This generates build_invoice_struct/0,1, insert_invoice!/0,1,2, and list variants, but
notbuild_invoice_params.
params?: false can also be set at the module level via use FactoryMan, params?: false.
Variant Factories (defvariant)
A variant wraps a base factory. It transforms params before the base factory runs (it is a preprocessor, not a postprocessor). The variant is defined after the base factory in your code, but executes before it at runtime:
deffactory user(params \\ %{}), struct: User do
%{username: sequence("user"), role: "member"}
|> Map.merge(params)
end
defvariant admin(params \\ %{}), for: :user do
Map.merge(%{role: "admin"}, params)
endCode order: deffactory user(...) -> defvariant admin(...), for: :user
Execution order: admin (preprocessor) -> user (base factory)
This generates build_admin_user_struct/0,1, insert_admin_user!/0,1,2, and list variants.
Calling build_admin_user_struct() is equivalent to build_user_struct(%{role: "admin"}).
Custom naming with :as
The :as option overrides the combined {variant}_{base} name:
defvariant moderator(params \\ %{}), for: :user, as: :mod do
Map.merge(params, %{role: "moderator"})
end
This generates build_mod_struct/0,1, insert_mod!/0,1,2, etc.
— instead of the default build_moderator_user_struct.
Documentation
Full documentation is available in the FactoryMan module.