FactoryMan

View Source

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

Inspired by ExMachina, but with a different API and feature set. See the examples below.

Quick Tour

Build factories

defmodule MyApp.Factory do
  use FactoryMan, repo: MyApp.Repo

  alias MyApp.Accounts.User
  alias MyApp.Blog.Post

  # Basic factory
  deffactory user(params \\ %{}), struct: User do
    base_params = %{
      username: sequence("user"),
      email: sequence(:email, fn n -> "user#{n}@example.com" end),
      role: sequence(:role, ["admin", "mod", "user"]),
      joined_at: fn -> DateTime.utc_now() end,
      display: fn u -> "#{u.username} (#{u.role})" end
    }

    Map.merge(base_params, params)
  end

  # Variant — preprocesses params, then delegates to the base factory
  defvariant admin(params \\ %{}), for: :user do
    base_params = %{role: "admin"}

    Map.merge(base_params, params)
  end

  # Associations — call other factories to build related records
  deffactory post(params \\ %{}), struct: Post do
    base_params = %{
      title: sequence("post", fn n -> "Post ##{n}" end),
      author: params[:author] || build_user_struct()
    }

    Map.merge(base_params, params)
  end

  # Params-only (no struct) — only generates build_*_params functions
  deffactory api_payload(params \\ %{}) do
    base_params = %{action: "create", resource: "user"}

    Map.merge(base_params, params)
  end
end

FactoryMan generates functions from your factories

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

iex> Factory.build_user_struct()
%User{username: "user1", email: "user1@example.com", ...}

iex> Factory.insert_user!()
%User{id: 1, username: "user2", ...}

iex> Factory.insert_user_list!(3)
[%User{id: 2, ...}, %User{id: 3, ...}, %User{id: 4, ...}]

iex> Factory.build_admin_user_struct()
%User{role: "admin", username: "user3", ...}

iex> Factory.build_api_payload_params()
%{action: "create", resource: "user"}

Installation

Add FactoryMan to your mix.exs dependencies:

def deps do
  [
    {:factory_man, "~> 0.3.0"}
  ]
end

Then run mix deps.get.

Defining Factories

Basic factories

Use deffactory to define a factory:

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

  Map.merge(base_params, params)
end

When using the :struct option, the factory body must return a plain map — the generated build_*_struct function converts it to a struct. If you need to return a struct directly, use build_params?: false instead. Without :struct, the body can return any value (see Arbitrary value factories).

You can name the parameter anything and use pattern matching:

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

  Map.merge(base_params, attrs)
end

Params-only factories

Omit the struct: option to create factories that only generate build_*_params functions:

deffactory api_payload(params \\ %{}) do
  %{action: "create", data: params}
end
# Generates: build_api_payload_params/0,1, build_api_payload_params_list/1,2

Arbitrary value factories

Without the :struct option, factory bodies can return any value — strings, keyword lists, tuples, or anything else:

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

deffactory search_opts(overrides \\ []) do
  Keyword.merge([page: 1, per_page: 20], overrides)
end
# Generates: build_greeting_params/0,1, build_search_opts_params/0,1 (and _list variants)

Lazy evaluation works in keyword lists the same way it does in maps.

Non-insertable factories

Use insert?: false to skip insert function generation, or let FactoryMan detect embedded schemas automatically:

deffactory read_only_user(params \\ %{}), struct: User, insert?: false do
  base_params = %{username: "readonly"}

  Map.merge(base_params, params)
end
# Generates: build_read_only_user_params, build_read_only_user_struct (and _list variants)
# Skips: insert_read_only_user!

Direct struct factories (build_params?: false)

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

deffactory invoice(params \\ %{}), struct: Invoice, build_params?: false do
  customer =
    case params[:customer] do
      %Customer{} = c -> c
      _ -> MyApp.Factory.Accounts.insert_customer!()
    end

  %Invoice{
    customer: customer,
    total: Map.get(params, :total, Enum.random(100..10_000))
  }
end
# Generates: build_invoice_struct, insert_invoice! (and _list variants)
# Skips: build_invoice_params

build_params?: false can also be set at the module level via use FactoryMan, build_params?: false. Non-struct factories in the same module are unaffected — their build_* functions are always generated.

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

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

Params For

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

params_for_user(%{username: "alice"})
# => %{username: "alice", first_name: nil, ...}
# (no __struct__, __meta__, autogenerated :id, or NotLoaded associations)

string_params_for_user(%{username: "alice"})
# => %{"username" => "alice", "first_name" => nil, ...}

belongs_to associations are removed from the output. If the association is persisted, the foreign key is set automatically. Nil values are preserved.

Sequences

Generate unique values across builds:

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

Reset sequences in test setup:

setup do
  FactoryMan.Sequence.reset()
  :ok
end

Lazy Evaluation

Functions in factory params are evaluated at build time:

%{
  # 0-arity: called with no arguments
  created_at: fn -> DateTime.utc_now() end,

  # 1-arity: receives the parent map (before lazy evaluation)
  display_name: fn user -> "#{user.username} (User)" end
}

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

Variant Factories

A variant wraps a base factory. It transforms params before the base factory runs (it is a preprocessor, not a postprocessor):

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
Code 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 default {variant}_{base} name:

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

  Map.merge(base_params, params)
end
# Generates: build_mod_struct/0,1, insert_mod!/0,1,2, etc.

Hooks

Hooks let you transform data at each stage of the build pipeline:

build_*_params:
  before_build_params -> [factory body + lazy eval] -> after_build_params

build_*_struct (calls build_*_params internally):
  -> before_build_struct -> struct!() -> after_build_struct

insert_*! (calls build_*_struct internally):
  -> before_insert -> Repo.insert!() -> after_insert

Available hooks:

HookReceivesPurpose
before_build_paramsparams mapTransform params before the factory body runs
after_build_paramsparams mapModify params after the factory body
before_build_structparams mapLast chance to modify params before struct!()
after_build_structstructTransform the struct after creation
before_insertstructModify struct just before database insertion
after_insertstructPost-process after insertion (e.g. reset associations)

Set hooks at the module level or per-factory:

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
deffactory user(params \\ %{}), struct: User,
  hooks: [after_build_params: &IO.inspect(&1, label: "user params")] do
  base_params = %{username: "user-#{System.os_time()}"}

  Map.merge(base_params, params)
end

Factory Inheritance

The extends: option

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

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

  # Inherits :repo and generate_username/0 from parent
  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.

Keep the base factory focused on shared config (repo, hooks, helpers). Child factories 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)

When to Use What

  • 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 when the record doesn't need to exist in the database yet.

  • insert_*! — When a foreign key constraint requires the record to exist, or when the test queries the database for it.

  • Lazy 0-arity — When a default is expensive to compute or depends on runtime state.

  • Lazy 1-arity — When a field's default depends on another field in the same factory.

A common mistake is inserting records when a plain struct would suffice. If you only need an ID for a foreign key, consider whether the test actually needs that constraint enforced.

Documentation

Full documentation is available on HexDocs.