Cookbook

View Source

This guide is a practical path from a first factory to the patterns that tend to appear in a real test suite. It uses the same account-and-blog examples as the rest of the documentation so that you can move between the README, this cookbook, and the FactoryMan API reference without learning a new example domain each time.

The examples assume Ecto schemas such as MyApp.Accounts.User, MyApp.Blog.Post, and MyApp.Blog.Tag. Replace those modules and fields with the ones in your application.

Two conventions appear throughout the guide:

  • Struct factories receive a map, define defaults in base_params, and finish with Map.merge(base_params, params) so caller values win.
  • Helpers are called with the module name, such as FactoryMan.sequence(...) and FactoryMan.assoc(...). Only deffactory and defvariant are imported by use FactoryMan.

Start with one useful factory

A factory is an ordinary module that uses FactoryMan. Configure your repo once, alias the schema, and return a map of fields from the factory body:

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

  alias MyApp.Accounts.User

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

    Map.merge(base_params, params)
  end
end

The defaults make the factory useful without arguments, while the final merge keeps individual tests in control:

test "shows the member dashboard" do
  user = MyApp.Factory.build_user_struct(%{username: "alice"})

  assert user.username == "alice"
  assert user.role == "member"
  assert user.id == nil
end

FactoryMan builds the struct for you. Do not return %User{} from a normal struct factory; use body: :struct only when you deliberately need direct control over struct construction.

Choose the result that matches the test

A struct factory generates several related functions. Use the function that matches the boundary you are testing instead of building one representation and converting it by hand.

The test needsUse
An in-memory recordbuild_user_struct/0,1
An atom-keyed input mapbuild_user_params/0,1
A string-keyed request mapbuild_user_string_params/0,1
Several independently built valuesThe matching *_list function
A row that already exists in the databaseinsert_user/0,1,2

Use params when exercising a changeset or context boundary:

test "accepts valid registration attributes" do
  attrs = MyApp.Factory.build_user_params(%{username: "alice"})

  assert %Ecto.Changeset{valid?: true} =
           MyApp.Accounts.User.changeset(%MyApp.Accounts.User{}, attrs)
end

Use string params when the real caller supplies string keys, as a controller or API client would:

test "creates a user through the API", %{conn: conn} do
  params = MyApp.Factory.build_user_string_params(%{username: "alice"})

  conn = post(conn, ~p"/api/users", %{"user" => params})

  assert %{"username" => "alice"} = json_response(conn, 201)
end

Params builders first build the struct and then remove Ecto metadata and persistence-only fields. That is why they are preferable to calling Map.from_struct/1 yourself.

Use an insert when persistence is part of the behavior under test:

test "loads a user by username" do
  user = MyApp.Factory.insert_user(%{username: "alice"})

  assert MyApp.Repo.get_by!(MyApp.Accounts.User, username: "alice").id == user.id
end

Repo options can be passed as the final argument:

MyApp.Factory.insert_user(%{username: "alice"}, returning: true)

Build several independent values

List builders invoke the factory once per item. Sequences, lazy values, hooks, and association normalization therefore run independently for every result:

users = MyApp.Factory.build_user_struct_list(3, %{role: "moderator"})

assert length(users) == 3
assert Enum.all?(users, &(&1.role == "moderator"))
assert users |> Enum.map(& &1.username) |> Enum.uniq() |> length() == 3

The same pattern applies to params and inserts:

attrs_list = MyApp.Factory.build_user_params_list(3)
inserted_users = MyApp.Factory.insert_user_list(3, %{role: "moderator"})

If you modify an already-built struct before inserting it, keep the factory's insert hooks by using insert_user_struct/1,2 rather than calling the repo directly:

user = MyApp.Factory.build_user_struct()
edited_user = %{user | username: "edited"}

MyApp.Factory.insert_user_struct(edited_user)

Make defaults realistic

Useful defaults should resemble valid application data, avoid accidental collisions, and defer work that may be overridden by the caller.

Generate unique values with sequences

The simplest sequence prefixes a counter with a string:

FactoryMan.sequence("user")
# "user0", then "user1", then "user2", ...

A formatter gives the counter an application-specific shape:

FactoryMan.sequence(:email, fn n -> "user#{n}@example.com" end)
FactoryMan.sequence(:order, fn n -> "ORD-#{n}" end, start_at: 1000)

A list formatter cycles through a small set of values:

FactoryMan.sequence(:role, ["admin", "member", "guest"])

Cycling values are useful for varied data, but they are not unique. Use a formatter when a database constraint requires uniqueness.

Reset sequence state when a test asserts exact generated values:

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

Sequence state is shared, so tests that reset and assert exact sequence positions should not race with other tests using the same sequence names.

Compute values lazily

Function values are evaluated when the factory builds. A zero-arity function computes a fresh value, and a one-arity function receives the containing map:

deffactory user(params \\ %{}), struct: User do
  base_params = %{
    username: FactoryMan.sequence("user"),
    role: "member",
    joined_at: fn -> DateTime.utc_now() end,
    display_name: fn user -> "#{user.username} (#{user.role})" end
  }

  Map.merge(base_params, params)
end

This is useful in a test that cares about a value derived from another caller-controlled field:

user = MyApp.Factory.build_user_struct(%{username: "alice", role: "admin"})

assert user.display_name == "alice (admin)"

One-arity functions receive the parent map before lazy evaluation. They can safely read ordinary fields such as username and role, but they should not depend on another field whose value is also a function.

Lazy defaults also avoid work when a caller supplies an override. This matters most when the default builds another record:

base_params = %{
  author: fn -> MyApp.Factory.Accounts.build_user_struct() end,
  tags: []
}

Map.merge(base_params, params)

The author is built only when the default survives the final merge. An eager call such as author: build_user_struct() runs even when the caller supplies an author.

Name recurring scenarios with variants

A variant is a small preprocessor for a base factory. Use one when tests repeatedly need the same kind of record:

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

  Map.merge(base_params, params)
end

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

  Map.merge(base_params, params)
end

FactoryMan combines the variant and base names:

admin = MyApp.Factory.build_admin_user_struct()
guest = MyApp.Factory.insert_guest_user()

assert admin.role == "admin"
assert guest.role == "guest"

A variant runs before the base factory. With the canonical merge order, callers can still override the preset:

user = MyApp.Factory.build_admin_user_struct(%{role: "owner"})
assert user.role == "owner"

That behavior is useful for defaults. If the name promises an invariant that callers must not contradict, validate it explicitly as shown in Validated presets.

Variants may build on other variants:

defvariant senior(params \\ %{}), for: :admin_user do
  Map.merge(%{display_name: "Senior administrator"}, params)
end

MyApp.Factory.build_senior_admin_user_struct()

Use as: when the combined name would be awkward:

defvariant moderator(params \\ %{}), for: :user, as: :mod do
  Map.merge(%{role: "moderator"}, params)
end

MyApp.Factory.build_mod_struct()

Ecto relationships are where factory setup can become noisy. Prefer declarative associations for direct Ecto associations, and be explicit about whether the test needs an in-memory graph or rows that already exist in the database.

Accept nested params and existing structs

Suppose account and blog factories live in separate modules:

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

  alias MyApp.Accounts.User

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

    Map.merge(base_params, params)
  end
end

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

  alias MyApp.Blog.{Post, Tag}

  deffactory tag(params \\ %{}), struct: Tag do
    base_params = %{name: FactoryMan.sequence("tag")}

    Map.merge(base_params, params)
  end

  deffactory post(params \\ %{}),
    struct: Post,
    associations: [author: {MyApp.Factory.Accounts, :user}, tags: :tag] do
    base_params = %{
      title: FactoryMan.sequence("post", fn n -> "Post ##{n}" end),
      content: "A post written for a test",
      author: fn -> MyApp.Factory.Accounts.build_user_struct() end,
      tags: []
    }

    Map.merge(base_params, params)
  end
end

An atom such as :tag names a factory in the current module. A tuple such as {MyApp.Factory.Accounts, :user} names a factory in another module.

Callers can now supply nested params:

test "builds a post from nested API-style input" do
  post =
    MyApp.Factory.Blog.build_post_struct(%{
      title: "Testing with factories",
      author: %{username: "alice"},
      tags: [%{name: "elixir"}, %{name: "testing"}]
    })

  assert post.author.username == "alice"
  assert Enum.map(post.tags, & &1.name) == ["elixir", "testing"]
end

Existing structs are reused. A plural association may mix existing structs and params maps:

user = MyApp.Factory.Accounts.build_user_struct(%{username: "existing"})
tag = MyApp.Factory.Blog.build_tag_struct(%{name: "existing-tag"})

post =
  MyApp.Factory.Blog.build_post_struct(%{
    author: user,
    tags: [tag, %{name: "new-tag"}]
  })

assert post.author === user
assert hd(post.tags) === tag

Only association keys supplied by the caller are normalized. Missing keys remain missing until the factory body supplies its normal defaults. For a singular association, explicit nil stays nil; for a plural association, use [] for no related values.

Declarative associations build structs in memory. They do not automatically insert associated records. They support direct Ecto associations; embeds and :through associations are not supported.

Insert a dependency when the database requires it

Sometimes the schema only needs a foreign key and the related row must already exist. Make that database dependency clear in the factory:

deffactory audit_event(params \\ %{}), struct: AuditEvent do
  base_params = %{
    action: "user.created",
    user_id:
      Map.get_lazy(params, :user_id, fn ->
        MyApp.Factory.Accounts.insert_user().id
      end)
  }

  Map.merge(base_params, params)
end

Map.get_lazy/3 avoids inserting a user when the caller supplies user_id:

user = MyApp.Factory.Accounts.insert_user()
event = MyApp.Factory.insert_audit_event(%{user_id: user.id})

assert event.user_id == user.id

Use this pattern when persistence is genuinely required. For ordinary in-memory associations, build_*_struct keeps tests faster and makes the dependency smaller.

Use low-level resolvers for custom inputs

The declarative associations: option is the usual choice for direct Ecto relationships. The lower-level resolvers are useful for plain structs, custom builder functions, or helper functions that accept either an existing struct or params:

user =
  FactoryMan.assoc(
    user_or_params,
    &MyApp.Factory.Accounts.build_user_struct/1,
    struct: MyApp.Accounts.User
  )

tags =
  FactoryMan.assoc_list(
    tags_or_params,
    &MyApp.Factory.Blog.build_tag_struct/1,
    struct: MyApp.Blog.Tag
  )

FactoryMan.assoc/3 reuses an existing struct, builds a params map, and builds nil with default params unless on_nil: :keep is supplied. FactoryMan.assoc_list/3 resolves each item in order, accepts a mixture of structs and params maps, and treats an outer nil as an empty list.

Use inherit: %{...} to place defaults beneath supplied params. Use struct: when you want both incoming structs and builder results checked against an expected type.

These helpers receive the association value itself; they do not fetch a key from a containing params map. If you already know that a value is a params map, a direct factory call is simpler.

Model non-schema inputs

Factories are also useful for request payloads, job arguments, adapter options, and other values that are not structs. Omit struct: and return whatever shape the application consumes:

deffactory api_payload(params \\ %{}) do
  base_params = %{
    action: "create",
    resource: "user",
    request_id: fn -> System.unique_integer([:positive]) end
  }

  Map.merge(base_params, params)
end

deffactory request_options(overrides \\ []) do
  base_options = [
    timeout: 5_000,
    retries: 3,
    label: fn options -> "timeout-#{options[:timeout]}" end
  ]

  Keyword.merge(base_options, overrides)
end

Non-struct factories use the shorter build_* names:

payload = MyApp.Factory.build_api_payload(%{resource: "post"})
options = MyApp.Factory.build_request_options(timeout: 1_000)
payloads = MyApp.Factory.build_api_payload_list(3)

assert payload.resource == "post"
assert options[:label] == "timeout-1000"
assert length(payloads) == 3

They can return maps, keyword lists, strings, tuples, or any other value. They do not generate struct, params, or insert functions because there is no schema to provide those semantics.

Catch input mistakes with strict params

A misspelled key in a merge-style factory normally fails later during struct construction. A direct struct factory may ignore it entirely. Opt in to strict params when you want the factory boundary to report the mistake immediately:

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

  Map.merge(base_params, params)
end

Now a typo fails where it was introduced:

MyApp.Factory.build_user_struct(%{usernme: "alice"})
# ** (ArgumentError) unknown params [:usernme] for strict factory :user ...

Strict validation also applies through params builders, inserts, list builders, and variants. Set it once for a factory module when that is the desired default:

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

  # Account factories are strict unless one overrides the option.
end

A factory may intentionally accept an input that is not a struct field. Allow that input explicitly:

deffactory user_from_domain(params \\ %{}),
  struct: User,
  body: :struct,
  strict: [allow: [:domain]] do
  domain = Map.get(params, :domain, "example.com")

  %User{
    username: FactoryMan.sequence("derived-user"),
    email: "derived@#{domain}"
  }
end

Keys outside the struct fields and the allowlist still raise. Strict params are ignored for non-struct factories because those factories have no struct field set to validate against.

Organize a growing factory suite

A single factory module is convenient at first. As the application grows, keep shared configuration in a small parent module and organize child factories around application contexts.

test/support/
  factory.ex                    # repo, shared hooks, shared helpers
  factory/
    accounts.ex                 # user and account factories
    blog.ex                     # post and tag factories
    blog/comments.ex            # comment factories

Share configuration with extends:

The parent owns configuration that should be consistent across the suite:

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

  def reset_associations(%_{} = struct) do
    Ecto.reset_fields(struct, struct.__struct__.__schema__(:associations))
  end
end

Child modules inherit the repo, hooks, and public helper functions:

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

  alias MyApp.Accounts.User

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

    Map.merge(base_params, params)
  end
end

The after_insert hook resets loaded associations so an inserted result resembles a record returned by a fresh query. This prevents tests from accidentally depending on associations that happened to be present during construction.

Inheritance chains may have more than one level, and a child may override inherited options. Keep the parent focused on shared behavior; domain-specific factory definitions belong in the child modules that use them.

Use hooks for cross-cutting behavior

Hooks transform values at defined points in the build and insert pipeline. A factory-local hook is useful when a rule belongs to one kind of data:

defmodule MyApp.Factory.Events do
  use FactoryMan

  def add_test_source(params), do: Map.put_new(params, :source, "test")

  deffactory event(params \\ %{}),
    hooks: [before_build_params: &__MODULE__.add_test_source/1] do
    base_params = %{name: "user.created"}

    Map.merge(base_params, params)
  end
end

A module-level hook is better for behavior shared by every factory in that module. Parent, child, and factory hooks merge by hook name, with the more specific level taking precedence when the same hook is configured again.

For a normal struct factory, the build path is:

strict validation
→ before_build_params
→ association normalization
→ factory body and lazy evaluation
→ after_build_params
→ before_build_struct
→ struct!/2
→ after_build_struct

An insert continues with before_insert, the repo insert, and after_insert. This is also why insert_*_struct is preferable to a direct repo call after editing a built struct: it keeps the insert hooks in the path.

Handle specialized construction

Most factories should return params maps. The following tools are useful when the value does not follow the ordinary table-backed Ecto path.

Build embedded schemas

Embedded schemas use the normal struct and params builders, but FactoryMan skips insert functions automatically:

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

  alias MyApp.Accounts.Settings

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

    Map.merge(base_params, params)
  end
end

Use the result in tests just like another in-memory struct:

settings = MyApp.Factory.Settings.build_settings_struct(%{theme: "light"})
attrs = MyApp.Factory.Settings.build_settings_params()

assert settings.theme == "light"
assert attrs.notifications

There is no insert_settings because an embedded schema has no table of its own.

Return a struct directly with body: :struct

Use body: :struct when construction is genuinely easier after another struct has been built, or when the body needs control that a params map cannot express:

deffactory anonymized_user(params \\ %{}), struct: User, body: :struct do
  user = build_user_struct(params)

  %{user |
    username: "anonymous",
    email: "redacted@example.com",
    display_name: "Anonymous user"
  }
end

The full function family is still generated:

MyApp.Factory.build_anonymized_user_struct()
MyApp.Factory.build_anonymized_user_params()
MyApp.Factory.build_anonymized_user_struct_list(3)
MyApp.Factory.insert_anonymized_user()

Params-stage hooks are skipped because the body does not perform params-to-struct conversion. after_build_struct and insert hooks still run. In the wrapper above, an after_build_struct hook runs once inside build_user_struct/1 and again for the wrapping factory.

Use direct struct bodies sparingly. A normal params body is easier to extend, compose, and inspect.

Build presets that keep their promises

Variants are ideal for caller-overridable defaults. Two more specialized patterns help when a preset must transform a finished value or enforce an invariant.

Post-build presets

A variant cannot transform the finished struct because it runs before the base factory. Wrap the base builder in a direct struct factory when the transformation belongs after construction:

deffactory verified_user(params \\ %{}), struct: User, body: :struct do
  user = build_user_struct(params)

  %{user | verified_at: DateTime.utc_now()}
end

This retains params, list, and insert functions under the new factory name. Remember the after_build_struct double-run caveat when the wrapper calls another factory that has the same hook.

Validated presets

A variant's defaults may be overridden. If a name promises a property such as “published,” validate the merged params before delegating to the base factory:

defvariant published(params \\ %{}), for: :post do
  base_params = %{
    published_at: DateTime.utc_now(),
    draft: false
  }

  result_params = Map.merge(base_params, params)

  if result_params[:draft] or is_nil(result_params[:published_at]) do
    raise ArgumentError, "published posts require published_at and draft: false"
  end

  result_params
end

A contradictory call now fails at the factory boundary:

MyApp.Factory.Blog.build_published_post_struct(%{draft: true})
# ** (ArgumentError) published posts require published_at and draft: false

The validation sees raw params before lazy evaluation. Do not write a predicate that expects a function-valued field to have been resolved already.

Inspect factories when names are dynamic

Most tests should call generated functions directly. Reflection is useful for test helpers or tools that receive a factory name at runtime:

def build_named(factory_module, factory_name, params \\ %{}) do
  unless factory_name in factory_module.__factory_man__(:factories) do
    raise ArgumentError, "unknown factory #{inspect(factory_name)}"
  end

  if is_nil(factory_module.__factory_man__(:opts, factory_name)[:struct]) do
    raise ArgumentError, "factory #{inspect(factory_name)} does not build a struct"
  end

  apply(factory_module, :"build_#{factory_name}_struct", [params])
end

Checking __factory_man__(:factories) before constructing the function name limits dispatch to registered factories. Variants appear under their full registered names.

For debugging, inspect the resolved options at module or factory level:

MyApp.Factory.Accounts.__factory_man__(:opts)
MyApp.Factory.Accounts.__factory_man__(:opts, :user)

This can answer whether a child inherited the expected repo, hook, strict setting, or struct module without guessing from generated function names.

Habits that keep factories easy to use

  • Merge caller params last. Map.merge(base_params, params) makes defaults predictable and keeps tests in control.
  • Pass maps to struct factories. Keyword lists are appropriate only when the factory itself accepts and returns keyword-list data.
  • Use generated names. A struct factory named user generates build_user_struct, build_user_params, and insert_user; it does not generate build_user.
  • Return params from normal struct factories. Return a struct only with body: :struct.
  • Qualify helpers. Call FactoryMan.sequence, FactoryMan.assoc, and FactoryMan.assoc_list; use FactoryMan imports only the definition macros.
  • Build unless persistence matters. An in-memory graph is usually enough. Insert when a query, constraint, or foreign key requires a row.
  • Prefer lazy association defaults. They avoid unnecessary work when callers override a relationship.
  • Avoid recursive defaults. A default user that builds a post whose default author builds another user will never terminate.
  • Reset sequences only when exact positions matter. Most tests should assert behavior rather than the counter value.
  • Keep the cookbook for recipes and the API reference for exhaustive semantics. When an edge case matters, consult the FactoryMan module documentation.