Working with Associations
ExMachina handles Ecto associations in factories so you don’t have to manually create and link related records. This guide covers how to define factories with belongs_to, has_many, and other relationships.
belongs_to Associations
Use build to define the associated record in your factory. When you insert the parent, ExMachina automatically inserts the association first:
def article_factory do
%MyApp.Article{
title: sequence(:title, &"Article #{&1}"),
body: "Some content",
author: build(:user)
}
end
Now when you call insert(:article), ExMachina will:
- Build the article with a built user
- Insert the user into the database
- Set the
author_idforeign key on the article - Insert the article
article = insert(:article)
article.author # => %MyApp.User{id: 1, ...}
article.author_id # => 1
Overriding the Association
Pass the association as an override to use a specific record:
author = insert(:user, name: "Specific Author")
article = insert(:article, author: author)
article.author.name # => "Specific Author"
Why build Instead of insert?
Always use build (not insert) for associations in factory definitions. ExMachina handles inserting when needed. If you use insert inside a factory, the associated record gets inserted every time — even when you only call build(:article), which is unexpected.
# Good — association is built, inserted only when parent is inserted
def article_factory do
%MyApp.Article{
author: build(:user)
}
end
# Bad — user gets inserted into the database even with build(:article)
def article_factory do
%MyApp.Article{
author: insert(:user)
}
end
has_many Associations
For has_many relationships, you typically don’t define the children in the parent factory. Instead, create them in your test and pass the parent as an override:
def comment_factory do
%MyApp.Comment{
body: "Great article!",
article: build(:article)
}
end
# In your test
article = insert(:article)
comments = insert_list(3, :comment, article: article)
If you need a factory that always includes children, use a helper function:
def with_comments(article, count \\ 3) do
insert_list(count, :comment, article: article)
article
end
# Usage
article = insert(:article) |> with_comments(5)
Nested Associations
Associations can be nested. If a comment belongs to an article that belongs to a user, ExMachina handles the full chain:
def comment_factory do
%MyApp.Comment{
body: "A comment",
article: build(:article)
}
end
def article_factory do
%MyApp.Article{
title: "An article",
author: build(:user)
}
end
# Inserting a comment automatically creates the article and its author
comment = insert(:comment)
comment.article.author # => %MyApp.User{...}
Sharing a Parent Across Children
When creating multiple records that should share the same parent, pass it explicitly:
author = insert(:user)
articles = insert_list(3, :article, author: author)
# All three articles belong to the same author
Without the explicit override, each article would get its own auto-generated author.
Lazy Evaluation with Functions
Use anonymous functions to delay association creation. This is useful when the association depends on the parent’s attributes:
def account_factory do
%MyApp.Account{
plan: "premium",
owner: fn -> build(:user) end
}
end
The function is called when the factory is evaluated, not when it’s defined. You can also receive the parent record as an argument:
def account_factory do
%MyApp.Account{
plan: "premium",
owner: fn account ->
build(:user, vip: account.plan == "premium")
end
}
end
When overriding lazy attributes at call time, wrap the override in a function too:
insert_pair(:account, owner: fn -> build(:user, name: "Shared") end)
many_to_many Associations
For many-to-many relationships, create the associations separately and link them:
def article_factory do
%MyApp.Article{
title: sequence(:title, &"Article #{&1}"),
tags: []
}
end
def tag_factory do
%MyApp.Tag{
name: sequence(:tag, &"tag-#{&1}")
}
end
# In your test
tags = insert_list(2, :tag)
article = insert(:article, tags: tags)
How this works depends on your Ecto schema setup. If you use a join table with many_to_many, you may need to handle the join records separately.
Params with Associations
The params functions handle associations differently:
params_for/2stripsbelongs_toassociations and foreign keysparams_with_assocs/2insertsbelongs_toassociations and includes the foreign key IDshas_manyandhas_oneassociations are recursively converted to maps
# No association data — useful for create actions where the parent is set separately
params_for(:article)
# => %{title: "Article 0", body: "Some content"}
# Inserts author, includes author_id — useful for nested create forms
params_with_assocs(:article)
# => %{title: "Article 0", body: "Some content", author_id: 1}
# String keys for controller params
string_params_with_assocs(:article)
# => %{"title" => "Article 0", "body" => "Some content", "author_id" => 1}
Tips
- Use
buildin factory definitions for all associations. Let ExMachina decide when to insert. - Override associations in tests to control relationships precisely and avoid unnecessary database records.
- Use helper functions like
with_comments/2for common patterns that add children to a parent. - Share parents explicitly when multiple records need to reference the same associated record.