FactoryMan (Factory Man v0.4.1)

View Source

An Elixir library for generating test data. Define factories with deffactory, and FactoryMan generates functions for building params, structs, and database records.

Quick Start

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
end

Generated Functions

For a factory named :user with struct: User:

FunctionReturnsPurpose
build_user_params/0,1%{}Plain map (for changesets, APIs)
build_user_struct/0,1%User{}Struct in memory (not persisted)
insert_user!/0,1%User{}Inserted into database
params_for_user/0,1%{}Stripped params (no Ecto metadata)
string_params_for_user/0,1%{"" => ...}Stripped params with string keys
build_user_params_list/1,2[%{}, ...]List of params maps
build_user_struct_list/1,2[%User{}, ...]List of structs
insert_user_list!/1,2[%User{}, ...]List of inserted records

All functions accept optional params for customization. Insert functions also accept repo options. Each item in a list is evaluated independently (unique timestamps, sequences, etc.).

What gets generated depends on the options:

OptionsParamsStructInsert
struct: User (default)YesYesYes
No struct: optionYesNoNo
insert?: falseYesYesNo
build_struct?: falseYesNoNo
build_params?: falseNoYesYes
Embedded schemaYesYesNo

Defining Factories

The deffactory macro works like defining a function — specify a name, a parameter, and a body that returns a plain map:

deffactory user(params \\ %{}), struct: User do
  base_params = %{username: "user-#{System.os_time()}"}

  Map.merge(base_params, params)
end

You can name the parameter anything, and use pattern matching:

deffactory user_from_config(%{username: username} = params), struct: User do
  base_params = %{username: username}

  Map.merge(base_params, params)
end

Struct vs. Non-Struct Factories

The struct: option controls both what functions are generated and how they're named:

# Struct factory — generates build_user_params, build_user_struct, insert_user!, etc.
deffactory user(params \\ %{}), struct: User do
  base_params = %{username: "user-#{System.os_time()}"}
  Map.merge(base_params, params)
end

# Non-struct factory — generates build_api_payload and build_api_payload_list only
deffactory api_payload(params \\ %{}) do
  %{action: "create", data: params}
end
Factory typeGenerated functions
struct: User (:user)build_user_params, build_user_struct, insert_user!, etc.
No struct: (:api_payload)build_api_payload, build_api_payload_list

Non-struct factories use simplified names (build_* instead of build_*_params) because they can return any value — maps, strings, keyword lists, tuples, nil, etc.:

deffactory greeting(name \\ "world") do
  "Hello, #{name}!"
end

deffactory search_opts(overrides \\ []) do
  Keyword.merge([page: 1, per_page: 20], overrides)
end

Lazy evaluation works in keyword lists the same way it does in maps — 0-arity and 1-arity functions are resolved at build time. Non-map, non-keyword-list values are passed through unchanged.

Associations — call other factories to build related records:

deffactory author(params \\ %{}), struct: Author do
  base_params = %{
    name: "Test Author",
    user: params[:user] || build_user_struct()
  }

  Map.merge(base_params, params)
end

Params For

For Ecto schema factories, FactoryMan generates params_for_* and string_params_for_* functions that build a struct and strip Ecto metadata, returning a clean map suitable for changesets or controller tests:

# Returns %{username: "user-123", first_name: nil, ...}
# (no __struct__, __meta__, autogenerated :id, or NotLoaded associations)
params_for_user(%{username: "alice"})

# Same but with string keys: %{"username" => "alice", ...}
string_params_for_user(%{username: "alice"})

Unlike ExMachina, nil values are preserved (a nil field may be intentional), and struct values like DateTime are left untouched.

Factory Options

Options cascade: parent module -> child module -> individual factory.

Module-level (set with use FactoryMan):

  • :repo — Ecto repo for database operations
  • :extends — Parent factory module to inherit configuration from
  • :hooks — Hooks applied to all factories in the module
  • :suppress_duplicate_option_warning — Suppress warnings for redundant options

Factory-level (set with deffactory):

  • :struct — Ecto schema module (enables struct and insert functions)
  • :insert? — Set to false to skip insert functions
  • :build_struct? — Set to false to skip struct builders
  • :build_params? — Set to false to skip params builders (body returns struct directly). Only affects struct factories; ignored for non-struct factories.
  • :hooks — Merged with module-level hooks
  • :suppress_duplicate_option_warning — Suppress warnings for redundant options

Factory Inheritance

Child factories inherit the parent's repo, hooks, and helper functions via :extends:

defmodule MyApp.Factory do
  use FactoryMan, repo: MyApp.Repo
  def generate_username, do: "user-#{System.os_time()}"
end

defmodule MyApp.Factory.Accounts do
  use FactoryMan, extends: MyApp.Factory

  deffactory user(params \\ %{}), struct: User do
    base_params = %{username: generate_username()}

    Map.merge(base_params, params)
  end
end

Inheritance chains are unlimited — a child factory can itself be extended.

Sequences

Generate unique values across builds:

sequence("user")                                          # "user0", "user1", ...
sequence(:email, fn n -> "user#{n}@example.com" end)     # custom formatter
sequence(:role, ["admin", "moderator", "user"])            # cycles through list
sequence(:order, fn n -> "ORD-#{n}" end, start_at: 1000) # custom start value

Reset in test setup: FactoryMan.Sequence.reset()

Lazy Evaluation

Functions in factory params are evaluated at build time. This works in both maps and keyword lists:

# In maps
%{
  created_at: fn -> DateTime.utc_now() end,               # 0-arity: called with no args
  display_name: fn user -> "#{user.username} (User)" end  # 1-arity: receives parent map
}

# In keyword lists
[
  created_at: fn -> DateTime.utc_now() end,
  label: fn kw -> "timeout-#{kw[:timeout]}" end          # 1-arity: receives parent keyword list
]

Lazy evaluation ordering

1-arity functions receive the map or keyword list before lazy evaluation. Don't reference other lazy fields — they'll still be function references, not resolved values.

Hooks

Transform data at specific stages. Every factory action has both a before and after hook.

Hook Pipeline

Each generated function uses a subset of the pipeline. The full flow for insert_user! is:

build_user_params:
  before_build_params → [factory body + lazy eval] → after_build_params

build_user_struct (calls build_user_params internally):
  → before_build_struct → struct!() → after_build_struct

insert_user! (calls build_user_struct internally):
  → before_insert → Repo.insert!() → after_insert

Hook Reference

HookReceivesReturnsWhen to Use
:before_build_paramsparams (map)params (map)Transform or inject params before the factory body runs
:after_build_paramsparams (map)params (map)Modify params after the factory body (e.g. add computed fields)
:before_build_structparams (map)params (map)Last chance to modify params before struct!() is called
:after_build_structstructstructTransform the struct after creation (e.g. set virtual fields)
:before_insertstructstructModify struct just before database insertion
:after_insertstructstructPost-process after insertion (e.g. reset associations)

Hook Precedence

Hooks can be set at three levels. Later levels override earlier ones for the same hook key:

  1. Parent moduleuse FactoryMan, hooks: [...]
  2. Child moduleuse FactoryMan, extends: Parent, hooks: [...]
  3. Individual factorydeffactory name(params), hooks: [...]

Examples

Reset associations after insert (most common hook usage):

defmodule MyApp.Factory do
  use FactoryMan,
    repo: MyApp.Repo,
    hooks: [after_insert: &__MODULE__.reset_assocs/1]

  def reset_assocs(struct) do
    Ecto.reset_fields(struct, struct.__struct__.__schema__(:associations))
  end
end

Log factory usage for debugging:

deffactory user(params \\ %{}), struct: User,
  hooks: [after_build_params: &__MODULE__.log_params/1] do
  base_params = %{username: "user-1773210151534978681"}

  Map.merge(base_params, params)
end

def log_params(params) do
  IO.inspect(params, label: "factory params")
  params
end

Embedded Schemas

Factories for embedded schemas work like regular struct factories but without database insertion:

defmodule MyApp.Factories.Settings do
  use FactoryMan, extends: MyApp.Factory

  alias MyApp.Users.Settings

  deffactory settings(params \\ %{}), struct: Settings do
    base_params = %{
      theme: "dark",
      notifications: true
    }

    Map.merge(base_params, params)
  end
end

Embedded schemas generate build_*_params and build_*_struct functions only (as well as the matching *_list functions), but do not generate any insert_* functions.

Direct Struct Factories (build_params?: false)

For complex factories that need full control over struct construction, set build_params?: false. The factory body returns a struct directly instead of a params map, and no build_*_params functions are generated:

deffactory invoice(params \\ %{}), struct: Invoice, build_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 not build_invoice_params. The after_build_struct, before_insert, and after_insert hooks still run. The before_build_params, after_build_params, and before_build_struct hooks are skipped since there is no params-to-struct conversion stage.

build_params?: false can also be set at the module level with use FactoryMan, build_params?: false, then overridden per-factory with build_params?: true if needed. Non-struct factories in the same module are unaffected — their build_* functions are always generated.

Variant Factories (defvariant)

A variant wraps an existing base factory. It transforms the caller's params before passing them to the base factory. Think of it as a preprocessor: the variant runs first, then the base factory runs with the transformed params.

This ordering can be counterintuitive because the variant is defined after the base factory in your code, but its logic executes before the base factory at runtime:

Code order:     deffactory user(...)   ->  defvariant admin(...), for: :user
Execution order:  admin (preprocessor)  ->  user (base factory)

Example

deffactory user(params \\ %{}), struct: User do
  base_params = %{username: sequence("user"), role: "member"}

  Map.merge(base_params, params)
end

defvariant admin(params \\ %{}), for: :user do
  base_params = %{role: "admin"}

  Map.merge(base_params, params)
end

Calling build_admin_user_struct() is equivalent to build_user_struct(%{role: "admin"}). Calling build_admin_user_struct(%{role: "superadmin"}) passes %{role: "superadmin"} to the base factory because the caller's params override the variant defaults.

Generated functions follow the pattern {variant}_{base}: build_admin_user_params/0,1, build_admin_user_struct/0,1, insert_admin_user!/0,1,2, plus list variants.

Custom naming with :as

The :as option overrides the combined {variant}_{base} name:

defvariant moderator(params \\ %{}), for: :user, as: :mod do
  base_params = %{role: "moderator"}

  Map.merge(base_params, params)
end

This generates build_mod_struct/0,1, insert_mod!/0,1,2, etc. — instead of the default build_moderator_user_struct.

Duplicate Option Warnings

If a child factory module specifies an option that is already defined by its parent with the same value, FactoryMan will emit a compile-time warning. This helps catch redundant options that were likely copy-pasted from the parent.

To suppress the warning for a specific module or factory, add suppress_duplicate_option_warning: true to the options.

Debugging

FactoryMan generates debug functions showing configured options:

iex> MyApp.Factory._factory_opts()
[repo: MyApp.Repo]

iex> MyApp.Factories.Users._user_factory_opts()
[repo: MyApp.Repo, struct: User]

Summary

Functions

Warn at compile time if child opts contain options that are already defined by the parent with the same value.

Defines a factory that generates test data.

Defines a variant factory that wraps a base factory.

Evaluate lazy attributes in a map, struct, or keyword list.

The default handler for hooks. This function is a no-op, and simply returns the given value without any modifications.

Get the configured handler for a hook, or fall back to &FactoryMan.fallback_hook_handler/0.

Generates a sequence of strings.

Generates and returns a unique sequence.

Generates and returns a unique sequence with options.

Functions

_warn_duplicate_options(parent_opts, child_opts, context)

Warn at compile time if child opts contain options that are already defined by the parent with the same value.

This is a FactoryMan internal function — called from macro-generated code. Use the underscore prefix convention to signal that it is not part of the public API.

deffactory(factory_head, opts \\ [], list)

(macro)

Defines a factory that generates test data.

The deffactory macro creates a set of functions for building test data. It works like defining a function, where you specify the factory name and a parameter (typically params).

Options

  • :struct - The Ecto schema module to build structs from. When provided, generates struct and insert functions in addition to params builders.
  • :insert? - Set to false to skip generating insert functions (default: true when repo is configured and struct is insertable)
  • :build_struct? - Set to false to skip generating struct builder functions (default: true)
  • :hooks - A keyword list of hook functions to apply at different stages (see Hooks section)
  • :suppress_duplicate_option_warning - Set to true to suppress warnings when this factory specifies an option already defined by the module with the same value

Generated Functions

For a factory named user with struct: User, the following functions are generated:

  • build_user_params/1 - Returns plain params
  • build_user_struct/1 - Returns an unsaved struct
  • insert_user!/1 - Inserts into the database (when repo is configured)
  • params_for_user/1 - Stripped params map (when struct is an Ecto schema)
  • string_params_for_user/1 - Stripped params with string keys (when struct is an Ecto schema)
  • build_user_params_list/2 - Builds multiple items
  • build_user_struct_list/2 - Builds multiple structs
  • insert_user_list!/2 - Inserts multiple items (when repo is configured)

For a factory named greeting without struct:, simplified names are used:

  • build_greeting/1 - Returns the factory's value
  • build_greeting_list/2 - Builds multiple items

Examples

deffactory user(params \\ %{}), struct: User do
  base_params = %{
    username: sequence("user"),
    email: sequence(:email, fn n -> "user#{n}@example.com" end)
  }

  Map.merge(base_params, params)
end

iex> MyApp.Factory.build_user_params(%{username: "alice"})
%{username: "alice", email: "user0@example.com"}

iex> MyApp.Factory.insert_user!(%{role: "admin"})
%User{id: 1, username: "user1", email: "admin@example.com", role: "admin"}

defvariant(variant_head, opts, list)

(macro)

Defines a variant factory that wraps a base factory.

A variant is a preprocessor: it receives the caller's params, transforms them, and delegates to the base factory. The variant body runs before the base factory, not after.

Example

deffactory user(params \\ %{}), struct: User do
  base_params = %{username: sequence("user"), role: "member"}

  Map.merge(base_params, params)
end

defvariant admin(params \\ %{}), for: :user do
  base_params = %{role: "admin"}

  Map.merge(base_params, params)
end

# Generated: build_admin_user_struct/0,1, insert_admin_user!/0,1,2, etc.
# Calling build_admin_user_struct() is equivalent to:
#   build_user_struct(%{role: "admin"})

Options

  • :for - (atom, required) The name of the base factory to wrap (e.g. :user)

  • :as - Instead of the default <variant>_<base> structure used when generating factory functions, (e.g. build_admin_user_struct), you may specify a custom name to use when generating the factory functions (e.g. as: :admin -> build_admin_struct)

evaluate_lazy_attributes(factory)

@spec evaluate_lazy_attributes(any()) :: any()

Evaluate lazy attributes in a map, struct, or keyword list.

Functions with 0 arity are called with no arguments. Functions with 1 arity receive the parent factory (map, struct, or keyword list) as their argument.

Non-map, non-keyword-list values are passed through unchanged.

Examples

iex> FactoryMan.evaluate_lazy_attributes(
...> %{name: "test", timestamp: fn -> System.os_time() end}
...> )
%{name: "test", timestamp: 12345}

iex> FactoryMan.evaluate_lazy_attributes(
...>   %{first: "John", last: fn attrs -> attrs.first <> " Smith" end}
...> )
%{first: "John", last: "John Smith"}

iex> FactoryMan.evaluate_lazy_attributes(
...>   [timeout: 5000, created_at: fn -> DateTime.utc_now() end]
...> )
[timeout: 5000, created_at: ~U[2026-01-01 00:00:00Z]]

iex> FactoryMan.evaluate_lazy_attributes("plain string")
"plain string"

fallback_hook_handler(value)

The default handler for hooks. This function is a no-op, and simply returns the given value without any modifications.

Examples

iex> FactoryMan.fallback_hook_handler(123)
123

get_hook_handler(hooks, hook)

Get the configured handler for a hook, or fall back to &FactoryMan.fallback_hook_handler/0.

Examples

iex> hooks = [after_insert: &YourProject.Factories.Users.user_after_insert_handler/1]

iex> FactoryMan.get_hook_handler(hooks, :before_build)
&FactoryMan.fallback_hook_handler/0

iex> FactoryMan.get_hook_handler(hooks, :after_insert)
&YourProject.Factories.Users.user_after_insert_handler/1

sequence(name)

@spec sequence(String.t()) :: String.t()

Generates a sequence of strings.

The sequence name is used as the beginning of the string. For example, if you do sequence("joe"), you will get back "joe0", then "joe1", and so on.

Example

def user_factory do
  %{
    username: sequence("joe")
  }
end

If you want to customize the returned string you can use sequence/2.

sequence(name, formatter)

@spec sequence(any(), (integer() -> any()) | [...]) :: any()

Generates and returns a unique sequence.

If a formatter function is passed, it will be called with the current position of the sequence. You can also pass a list, and each item in the list will be returned in sequence.

Example with a formatter function

def user_factory do
  %{
    email: sequence(:email, fn n -> "me-#{n}@foo.com" end)
  }
end

Example with a list

def user_factory do
  %{
    name: sequence(:name, ["Joe", "Mike", "Sarah"])
  }
end

sequence(name, formatter, opts)

@spec sequence(any(), (integer() -> any()) | [...], [{:start_at, non_neg_integer()}]) ::
  any()

Generates and returns a unique sequence with options.

Currently, the only option is :start_at which specifies the number to start the sequence at.

Example

def money_factory do
  %{
    cents: sequence(:cents, fn n -> "#{n}" end, start_at: 600)
  }
end