FactoryMan builds test data. Factories are defined with deffactory and defvariant in a module
that has use FactoryMan, and FactoryMan generates the functions that build params, structs, and
database records.
Defining factories
Always follow this shape:
deffactory name(params \\ %{}), struct: SchemaModule do
base_params = %{field: "default_value"}
Map.merge(base_params, params)
end- With
struct:, the body returns a map of the struct's fields, not a struct. Withbody: :struct, the body returns the struct itself; params functions are still generated, and the struct is still lazily evaluated. - Without
struct:, the body can return any value (map, keyword list, string, tuple, ...), andbody:is ignored. - Merge
paramsyourself, last: FactoryMan does not merge them for you. - Struct factories take a map of params, never a keyword list.
- Only
deffactoryanddefvariantare imported. Call helpers qualified:FactoryMan.sequence/1,2,3,FactoryMan.assoc/3,4,FactoryMan.assoc_list/3,4. - Unknown options to
use FactoryMan,deffactory, anddefvariantraise.
Generated functions
For deffactory user(params \\ %{}), struct: User:
| Function | Returns |
|---|---|
build_user_struct/0,1,2 | %User{} in memory (not persisted) |
build_user_params/0,1,2 | Clean params map derived from struct |
build_user_string_params/0,1,2 | Same, with string keys |
insert_user/0,1,2 | %User{} inserted into the database |
insert_user_struct/1,2 | Inserts an already-built %User{} |
build_user_struct_list/1,2,3 | List of structs |
build_user_params_list/1,2,3 | List of params maps |
build_user_string_params_list/1,2,3 | List of string-keyed params maps |
insert_user_list/1,2,3 | List of inserted records |
- A struct factory has no
build_user/1. A factory withoutstruct:generates onlybuild_*andbuild_*_list. - With the default
insert: :ecto, embedded schemas, plain structs, and modules without arepo:get no insert functions.insert: falseremoves them. - A builder's last argument may be options; the only one is
variants:.insert_*takes one option list::variantsis FactoryMan's, and every other option goes to the insert target (Repo.insert!/2by default). - With
:ecto,insert_*_structraises on a struct that has already been inserted. disable: [family: true]switches off unused families::params,:string_params,:struct_list,:params_list,:string_params_list,:insert_list, and:non_struct_list. It cascades per key;family: falseenables an inherited family again.build_*_structis always generated, and inserts are switched off withinsert: false. The disabled families show in__factory_man__(:opts, name)[:disable].
Insert targets
use FactoryMan, repo: MyApp.Repo, insert_via: [search: &MyApp.Factory.index!/2]
deffactory event(params \\ %{}), struct: Event, insert: &MyApp.Factory.publish!/2 do
Map.merge(%{name: "event"}, params)
end
insert_event() # MyApp.Factory.publish!/2, with the insert hooks
insert_event_via_search() # MyApp.Factory.index!/2, no hooks
insert_event_struct_via_search(event)insert:is the default target, used byinsert_*,insert_*_list, andinsert_*_struct::ecto(default),false, or a remote capture of arity 2. A capture works for any struct factory, including plain structs and embedded schemas.insert_via: [name: capture]adds aninsert_*_via_<name>family (/0,1,2,_list/1,2,3, andinsert_*_struct_via_<name>/1,2) to every struct factory below it.name: falseremoves an inherited target.ectois not a valid name.- An insert function takes
(struct, opts)and returns the inserted struct. Writedef put!(struct, _opts)for a store without options. Anonymous functions and local captures raise. - Insert hooks run for the default target only. A target does everything it needs in its own function, and does not check whether the struct has already been inserted.
- Set
insert:andinsert_via:on struct factories only; on a non-struct factory they raise.
Variants
defvariant admin(params \\ %{}), for: :user do
Map.merge(%{role: "admin"}, params)
end
defvariant senior(params \\ %{}), for: :user, extends: [:admin] do
Map.merge(%{title: "Senior admin"}, params)
end
build_admin_user_struct()
build_user_struct(%{}, variants: [:admin, :confirmed])- A variant preprocesses params, then delegates to its base factory. Its functions are named
{variant}_{base}(e.g.build_admin_user_struct), or byas:. for:must name a factory. Build on other variants of the same factory withextends:, and define a variant after the variants it extends, in the same module as its base factory.- Combine variants in one build with
variants: [...], listing variants by theirdefvariantname (:admin, not:admin_user). - Precedence: the caller's params win, then a later variant wins over an earlier one, and a variant wins over the variants it extends. This holds for variants that merge params last.
- A variant that merges params first (
Map.merge(params, %{banned: true})) forces its values: it wins over the caller and over later variants. Use it only for presets whose name is a promise. - A variant that only sets values needs no body:
defvariant admin, for: :user, defaults: %{role: "admin"}.defaults:loses to the caller,force:wins over the caller, and a variant with onlyextends:names a combination. Add:factory_mantoimport_depsin.formatter.exssomix formatleaves them without parentheses. - List a factory's variant names with
__factory_man__(:variants, factory_name).
Associations
- Declare associations with
assocs: [author: &build_user_struct/1](Ecto schemastruct:only; direct associations only, no embeds or:through). - The builder is the default: an absent key is built with
%{}([]for a list). Never put a default for a declared key inbase_params; it is never used. - The body always receives declared keys resolved, so
Map.merge(base_params, params)stays correct. - Builder forms: a 1-arity function; a 2-arity
fn params, factory_params -> ... endthat sees the keys declared above it resolved; or{builder, default: value, required: true}. default:is evaluated on every build, so keep it a literal (nil, a params map, or a list of params maps).paramsis not in scope inassocs:.required: true(singular keys only) raises on a caller's or builder'snil.- A caller's explicit
nilis kept; anillist raises. A self-referential default build raises; declare such a key withdefault: nil(ordefault: []). FactoryMan.assoc/3,4andFactoryMan.assoc_list/3,4resolve one key imperatively and do not changeparams: put the resolved value back (Map.put(params, :author, author)) or drop the key before the final merge.build_*_paramssets abelongs_toforeign key only when the associated record is inserted. For a real foreign key, pass an inserted record, or use a variant whose builder inserts.
Strict params
strict: trueraises on unknown keys (before the body) and on params the body ignores or changes (after the body). Set it once in the base factory:use FactoryMan, strict: true.strict: [allow: [:key]]exempts keys from both checks: a non-field key may be passed, and a field may be changed by the body.- A plain map field is kept when each key the caller gave is kept. Function values and association keys are not checked.
- Strict is ignored for non-struct factories. The ignored-params check runs in the base factory, not in variant bodies.
Hooks
- A hook is a 1-arity remote capture (
&__MODULE__.my_hook/1) or a list of them. Anonymous functions and local captures raise. - Hook names:
before_build_params,after_build_params,before_build_struct,after_build_struct,before_insert,after_insert. - Hooks chain across levels (parent module, child module, factory) in onion order: a parent's
before_*hooks run first, itsafter_*hooks run last. Place hooks with
{hook, :before_parent | :after_parent | :replace_parent}. Switch off the inherited hooks with{[], :replace_parent}.
Lazy values and sequences
- In a body's map or keyword list, a 0-arity function is called at build time, and a 1-arity function receives the parent map. 0-arity functions resolve first, so a 1-arity function sees plain and 0-arity values, but not another 1-arity field.
- Use a lazy
fn -> ... endfor a value that should only be computed when the caller does not supply one. FactoryMan.sequence("user")gives"user0","user1", ...;sequence(name, formatter)andsequence(name, list)format or cycle.FactoryMan.Sequence.reset/0resets every counter for every running test; in async tests usereset/1with the test's own names.
Anti-patterns
- A keyword list as params to a struct factory.
- A factory body that forgets
Map.merge(base_params, params). build_user()for a struct factory (usebuild_user_struct(),build_user_params(), orinsert_user()).- A struct returned from a body without
body: :struct. - A
base_paramsdefault for a key declared inassocs:. - Factories defined outside a module that uses
FactoryMan. Repo.insert!/2on a built struct instead ofinsert_*_struct, which skips the insert hooks.- An
insert:orinsert_via:function of arity 1, or one that returns something other than the inserted struct.