A toolkit for test data generation.
The main idea of SeedFactory is to generate data in tests according to your application business logic (read as
context functions if you use Phoenix Contexts) whenever possible
and avoid direct inserts to the database (as opposed to ex_machina).
This approach allows you to minimize testing of invalid states as you're not forced to keep complex database structure in your head in order to prepare test data.
The library is completely agnostic to the database toolkit.
Core concepts
Context is a map/0 which can be populated with entities using commands.
The schema with instructions on how commands modify context is described using DSL with the help of SeedFactory.Schema module.
Commands can be used to:
- produce entity (put new data in the context)
- update entity (replace the existing entity in the context)
- delete entity (remove the entity from the context)
A command has params with instructions on how to generate arguments for a resolver if they are not passed explicitly with exec/3 function.
The instruction can be specified using one of these options:
:value- any term. This option is used by default with the value ofnil.:generate- a zero-arity function for generating data.:entity- an atom which points to an entity which should be taken from the context. If a required entity cannot be found in a context, thenSeedFactoryautomatically executes a corresponding command which produces the entity. The:entityoption also supports:with_traitsto require that the entity has specific traits before it can be used as a dependency.
Traits are labels assigned to entities when specific commands with specific arguments are executed.
They describe how an entity was created or what state it is in. Traits can build on each other using from —
for example, an :active trait can require :pending first, meaning the entity must go through the :pending
state before becoming :active. Traits can also be tied to specific argument values using args_pattern.
Schema example
defmodule MyApp.SeedFactorySchema do
use SeedFactory.Schema
command :create_company do
param :name, generate: &Faker.Company.name/0
resolve(fn args ->
with {:ok, company} <- MyApp.Companies.create_company(args) do
{:ok, %{company: company}}
end
end)
produce :company
end
command :create_user do
param :name, generate: &Faker.Person.name/0
param :role, value: :normal
param :company, entity: :company
resolve(fn args -> MyApp.Users.create_user(args.company, args.name, args.role) end)
produce :user
produce :profile
end
command :activate_user do
# with_traits ensures the user has the :pending trait before activation
param :user, entity: :user, with_traits: [:pending]
resolve(fn args ->
user = MyApp.Users.activate_user!(args.user)
{:ok, %{user: user}}
end)
update :user
end
# :pending is assigned when :create_user is executed
trait :pending, :user do
exec :create_user
end
# :active requires :pending first — :activate_user replaces :pending with :active
trait :active, :user do
from :pending
exec :activate_user
end
# :admin and :normal are determined by the :role argument
trait :admin, :user do
exec :create_user, args_pattern: %{role: :admin}
end
trait :normal, :user do
exec :create_user, args_pattern: %{role: :normal}
end
endGetting started
To start using the schema, put metadata about it to the context using init/2 function:
context = %{}
context = init(context, MyApp.SeedFactorySchema)If you use SeedFactory in tests with ExUnit, check out SeedFactory.Test. This module adds initialization using ExUnit.Callbacks.setup_all/2 callback
and imports functions.
exec
exec/2 function can be used to execute a command:
context = exec(context, :create_company)The code above will generate arguments for :create_company command, execute a resolver with generated arguments and put company to context using :company key.
exec/3 can be used if you want to specify parameters explicitly:
context = exec(context, :create_company, name: "GitHub")Because exec function returns context/0, it is convenient to chain exec calls with the pipe operator:
context =
context
|> exec(:create_company)
|> exec(:create_user, name: "John Doe")In order to get a value for the :company parameter of the :create_user command, the corresponding entity was taken from the context.
However, it is not necessary to do so, as SeedFactory can automatically execute commands which produce dependent entities.
The code above has the same effect as a single call to :create_user command:
context = exec(context, :create_user, name: "John Doe")produce
If you're not interested in explicitly providing parameters to commands, you can use produce/2 to produce
requested entities with automatic execution of all dependent commands:
context = produce(context, :user)Even though :user is the only entity specified explicitly, context will have 3 new keys: :company, :user and :profile.
Traits can be specified to control how entities are created. SeedFactory figures out the command chain from the trait definitions:
# these two are equivalent
context = produce(context, user: [:admin, :active])
context =
context
|> exec(:create_user, role: :admin)
|> exec(:activate_user)Use the :as option to combine traits with rebinding:
%{active_admin: active_admin} = produce(context, user: [:admin, :active, as: :active_admin])Tip
Pattern match on what you pass to produce/2. This makes the test self-documenting
and avoids relying on implicitly created entities:
# good — matches what was requested
%{user: user} = produce(context, :user)
# good — multiple entities requested and matched
%{user: user, profile: profile} = produce(context, [:user, :profile])
# bad — matching entities that were not explicitly requested
%{user: user, profile: profile, company: company} = produce(context, :user)Rebinding
exec/3 fails if produced entities are already present in the context.
It is possible to rebind entities in order to assign them to the context with different names:
context =
context
|> rebind([user: :user1, profile: :profile1], &exec(&1, :create_user))
|> rebind([user: :user2, profile: :profile2], &exec(&1, :create_user))The snippet above puts the following keys to the context: :company, :user1, :profile1, :user2 and :profile2.
The :company is shared in this case, so two users have different profiles and belong to the same company.
A shorter counterpart using produce/2 is the following:
context =
context
|> produce(user: :user1, profile: :profile1)
|> produce(user: :user2, profile: :profile2)Debugging
SeedFactory tracks how each entity was created and modified.
You can inspect __seed_factory_meta__ key in the context to review currently assigned traits and execution history:
context |> produce(user: [:admin, :active]) |> IO.inspect()
# %{
# __seed_factory_meta__: #SeedFactory.Meta<
# current_traits: %{company: [], profile: [], user: [:admin, :active]},
# trails: %{
# company: #trail[:create_company],
# profile: #trail[:create_user],
# user: #trail[create_user: +[:pending, :admin] -> activate_user: +[:active] -[:pending]]
# },
# execution_history: [
# #execution[produce(user: [:admin, :active]): create_company → create_user → activate_user]
# ],
# ...
# >,
# company: %Company{...},
# profile: %Profile{...},
# user: %User{...}
# }If an exception or error occurs during command execution, SeedFactory raises SeedFactory.ExecError
with structured metadata including the execution plan, trails, and current traits at the time of failure:
** (SeedFactory.ExecError) unable to execute :create_user command: :email_already_taken
Execution plan:
✔ :create_company
✖ :create_user
Current traits:
%{company: []}
Summary
Functions
Executes a command and updates the context according to the schema.
Puts metadata about schema to context, so context becomes usable by other functions from this module.
Creates all dependent entities needed for a command, without executing the command itself.
Produces all dependencies needed for specified entities, without producing the entities themselves.
Produces entities with their dependencies resolved automatically.
Creates a scoped context where entities are assigned under different names.
Types
@type context() :: map()
@type entity_name() :: atom()
@type rebinding_rule() :: {entity_name(), rebind_as :: atom()}
Functions
Executes a command and updates the context according to the schema.
Example
iex> context = %{}
...> context = init(context, MySeedFactorySchema)
...> context = exec(context, :create_user, first_name: "John", last_name: "Doe")
...> Map.take(context.user, [:first_name, :last_name])
%{first_name: "John", last_name: "Doe"}
Puts metadata about schema to context, so context becomes usable by other functions from this module.
Example
iex> context = %{}
...> init(context, MySeedFactorySchema)
%{__seed_factory_meta__: #SeedFactory.Meta<...>}
Creates all dependent entities needed for a command, without executing the command itself.
This is useful when you want to execute the same command multiple times with shared dependencies.
It is especially handy when a command has many dependencies — pre_exec resolves all of them
automatically, so you don't have to enumerate them yourself.
Example
Consider a schema where :publish_article depends on :blog, :category,
and :author (with trait :active):
command :publish_article do
param :blog, entity: :blog
param :category, entity: :category
param :author, entity: :author, with_traits: [:active]
param :title, generate: &Faker.Lorem.sentence/0
# ...
produce :article
endWithout pre_exec, you would have to manually set up all the dependencies:
context = produce(context, [:blog, :category, author: [:active]])
%{article: article1} = exec(context, :publish_article, title: "First")
%{article: article2} = exec(context, :publish_article, title: "Second")With pre_exec, the same is achieved without knowing the dependency tree:
context = pre_exec(context, :publish_article)
%{article: article1} = exec(context, :publish_article, title: "First")
%{article: article2} = exec(context, :publish_article, title: "Second")
@spec pre_produce( context(), entity_name() | [ entity_name() | rebinding_rule() | {entity_name(), [trait_name :: atom() | {:as, rebind_as :: atom()}]} ] ) :: context()
Produces all dependencies needed for specified entities, without producing the entities themselves.
Works like pre_exec/3, but operates on entities instead of commands. The dependency tree
is derived from the commands that would produce the requested entities.
When traits are specified, the full command chain is analyzed — including update commands.
All their dependencies are created, but the requested entity itself is not.
For example, if :activate_user updates :user and depends on :team,
then pre_produce(context, user: [:active]) will create :team (and all other
dependencies in the chain) without creating the :user.
Examples
# Create all dependencies for :article (e.g. :blog, :category, :author)
# without creating the article itself
context = pre_produce(context, :article)
%{article: article1} = produce(context, :article)
%{article: article2} = produce(context, :article)Accepts a list with traits, same as produce/2:
context = pre_produce(context, [:article, user: [:active, :admin]])
@spec produce( context(), entity_name() | [ entity_name() | rebinding_rule() | {entity_name(), [trait_name :: atom() | {:as, rebind_as :: atom()}]} ] ) :: context()
Produces entities with their dependencies resolved automatically.
Analyzes the schema to determine which commands need to be executed and in what order. Entities that already exist in the context are reused. The order of specified entities doesn't matter.
Examples
# produce a single entity (dependencies like :company are created automatically)
%{user: _} = produce(context, :user)
# produce multiple entities
%{user: _, company: _} = produce(context, [:user, :company])
# rebind entity to a different name
%{user1: _} = produce(context, user: :user1)
# specify traits
%{user: _} = produce(context, user: [:active, :admin])
# apply traits incrementally — first create a pending admin, then activate
%{user: _} = context |> produce(user: [:pending, :admin]) |> produce(user: [:active])
# combine traits with rebinding using :as option
%{user1: _} = produce(context, user: [:active, :admin, as: :user1])Multiple commands producing the same entity
If the same entity can be produced by multiple commands, the first declared command is used by default. Traits allow you to pick a specific command. For example, given:
command :create_draft_project do
# ...
produce :draft_project
end
command :import_draft_project do
# ...
produce :draft_project
end
trait :created, :draft_project do
exec :create_draft_project
end
trait :imported, :draft_project do
exec :import_draft_project
endThen:
# uses :create_draft_project (first declared) and assigns the :created trait
produce(context, :draft_project)
# equivalent to the above, but explicit
produce(context, draft_project: [:created])
# uses :import_draft_project
produce(context, draft_project: [:imported])
@spec rebind(context(), [rebinding_rule()], (context() -> context())) :: context()
Creates a scoped context where entities are assigned under different names.
Within the callback, all commands that produce, update, delete, or depend on the specified entities will use the remapped names. The rebinding is scoped to the callback — it does not affect the context outside of it.
This is useful when you need to produce the same entity multiple times and
control how they relate to each other. For simpler cases where you don't need
to control relationships, produce/2 with rebinding is enough:
context |> produce(company: :company1) |> produce(company: :company2)Example
# :product requires :company. Here we create 2 companies and 1 product.
# Inside the first rebind, :company refers to :company1 and :product to :product1,
# so the product is linked to company1.
%{company1: _, company2: _, product1: _} =
context
|> rebind([company: :company1, product: :product1], fn context ->
context
|> exec(:create_company, name: "GitHub")
|> produce(:product)
end)
|> rebind([company: :company2], fn context ->
exec(context, :create_company, name: "Microsoft")
end)