beam-community/ex_machina
{ "createdAt": "2015-09-14T15:40:28Z", "defaultBranch": "main", "description": "Create test data for Elixir applications", "fullName": "beam-community/ex_machina", "homepage": "https://hex.pm/packages/ex_machina", "language": "Elixir", "name": "ex_machina", "pushedAt": "2025-11-24T22:31:06Z", "stargazersCount": 2034, "topics": [ "elixir", "factories", "factory-definitions", "testing" ], "updatedAt": "2025-11-24T22:30:48Z", "url": "https://github.com/beam-community/ex_machina"}ExMachina
Section titled “ExMachina”ExMachina makes it easy to create test data and associations. It works great with Ecto, but is configurable to work with any persistence library.
This README follows the main branch, which may not be the currently published version. Here are the docs for the latest published version of ExMachina.
Installation
Section titled “Installation”In mix.exs, add the ExMachina dependency:
def deps do [ {:ex_machina, "~> 2.8.0", only: :test}, ]endAdd your factory module inside test/support so that it is only compiled in the
test environment.
Next, be sure to start the application in your test/test_helper.exs before
ExUnit.start:
{:ok, _} = Application.ensure_all_started(:ex_machina)Install in just the test environment for non-Phoenix projects
Section titled “Install in just the test environment for non-Phoenix projects”You will follow the same instructions as above, but you will also need to add
test/support to your compilation paths (elixirc_paths) if you have not done
so already.
In mix.exs, add test/support to your elixirc_paths for just the test env.
def project do [app: ..., # Add this if it's not already in your project definition. elixirc_paths: elixirc_paths(Mix.env)]end
# This makes sure your factory and any other modules in test/support are compiled# when in the test environment.defp elixirc_paths(:test), do: ["lib", "test/support"]defp elixirc_paths(_), do: ["lib"]Overview
Section titled “Overview”Check out the docs for more details.
Define factories:
defmodule MyApp.Factory do # with Ecto use ExMachina.Ecto, repo: MyApp.Repo
# without Ecto use ExMachina
def user_factory do %MyApp.User{ name: "Jane Smith", email: sequence(:email, &"email-#{&1}@example.com"), role: sequence(:role, ["admin", "user", "other"]), } end
def article_factory do title = sequence(:title, &"Use ExMachina! (Part #{&1})") # derived attribute slug = MyApp.Article.title_to_slug(title) %MyApp.Article{ title: title, slug: slug, # another way to build derived attributes tags: fn article -> if String.contains?(article.title, "Silly") do ["silly"] else [] end end, # associations are inserted when you call `insert` author: build(:user) } end
# derived factory def featured_article_factory do struct!( article_factory(), %{ featured: true, } ) end
def comment_factory do %MyApp.Comment{ text: "It's great!", article: build(:article), } endendUsing factories (check out the docs for more details):
# `attrs` are automatically merged in for all build/insert functions.
# `build*` returns an unsaved comment.# Associated records defined on the factory are built.attrs = %{body: "A comment!"} # attrs is optional. Also accepts a keyword list.build(:comment, attrs)build_pair(:comment, attrs)build_list(3, :comment, attrs)
# `insert*` returns an inserted comment. Only works with ExMachina.Ecto# Associated records defined on the factory are inserted as well.insert(:comment, attrs)insert_pair(:comment, attrs)insert_list(3, :comment, attrs)
# `params_for` returns a plain map without any Ecto specific attributes.# This is only available when using `ExMachina.Ecto`.params_for(:comment, attrs)
# `params_with_assocs` is the same as `params_for` but inserts all belongs_to# associations and sets the foreign keys.# This is only available when using `ExMachina.Ecto`.params_with_assocs(:comment, attrs)
# Use `string_params_for` to generate maps with string keys. This can be useful# for Phoenix controller tests.string_params_for(:comment, attrs)string_params_with_assocs(:comment, attrs)Delayed evaluation of attributes
Section titled “Delayed evaluation of attributes”build/2 is a function call. As such, it gets evaluated immediately. So this
code:
insert_pair(:account, user: build(:user))Is equivalent to this:
user = build(:user)insert_pair(:account, user: user) # same user for both accountsSometimes that presents a problem. Consider the following factory:
def user_factory do %{name: "Gandalf", email: sequence(:email, &"gandalf#{&1}@istari.com")}endIf you want to build a separate user per account, then calling
insert_pair(:account, user: build(:user)) will not give you the desired
result.
In those cases, you can delay the execution of the factory by passing it as an anonymous function:
insert_pair(:account, user: fn -> build(:user) end)You can also do that in a factory definition:
def account_factory do %{user: fn -> build(:user) end}endYou can even accept the parent record as an argument to the function:
def account_factory do %{user: fn account -> build(:user, vip: account.premium) end}endNote that the account passed to the anonymous function is only the struct
after it’s built. It’s not an inserted record. Thus, it does not have data that
is only accessible after being inserted into the database (e.g. id).
Full control of factory
Section titled “Full control of factory”By default, ExMachina will merge the attributes you pass into build/insert into your factory. But if you want full control of your attributes, you can define your factory as accepting one argument, the attributes being passed into your factory.
def custom_article_factory(attrs) do title = Map.get(attrs, :title, "default title")
article = %Article{ author: "John Doe", title: title }
# merge attributes and evaluate lazy attributes at the end to emulate # ExMachina's default behavior article |> merge_attributes(attrs) |> evaluate_lazy_attributes()endNOTE that in this case ExMachina will not merge the attributes into your factory, and it will not evaluate lazy attributes. You will have to do this on your own if desired.
Non-map factories
Section titled “Non-map factories”Because you have full control of the factory when defining it with one argument, you can build factories that are neither maps nor structs.
# factory definitiondef room_number_factory(attrs) do %{floor: floor_number} = attrs sequence(:room_number, &"#{floor_number}0#{&1}")end
# example usagebuild(:room_number, floor: 5)# => "500"
build(:room_number, floor: 5)# => "501"NOTE that you cannot use non-map factories with Ecto. So you cannot
insert(:room_number).
Usage in a test
Section titled “Usage in a test”# Example of use in Phoenix with a factory that uses ExMachina.Ectodefmodule MyApp.MyModuleTest do use MyApp.ConnCase # If using Phoenix, import this inside the using block in MyApp.ConnCase import MyApp.Factory
test "shows comments for an article" do conn = conn() article = insert(:article) comment = insert(:comment, article: article)
conn = get conn, article_path(conn, :show, article.id)
assert html_response(conn, 200) =~ article.title assert html_response(conn, 200) =~ comment.body endendWhere to put your factories
Section titled “Where to put your factories”If you are using ExMachina in all environments:
Start by creating one factory module (such as
MyApp.Factory) inlib/my_app/factory.exand putting all factory definitions in that module.
If you are using ExMachina in only the test environment:
Start by creating one factory module (such as
MyApp.Factory) intest/support/factory.exand putting all factory definitions in that module.
Later on you can easily create different factories by creating a new module in the same directory. This can be helpful if you need to create factories that are used for different repos, your factory module is getting too big, or if you have different ways of saving the record for different types of factories.
Splitting factories into separate files
Section titled “Splitting factories into separate files”This example shows how to set up factories for the testing environment. For setting them in all environments, please see the To install in all environments section
Start by creating main factory module in
test/support/factory.exand name itMyApp.Factory. The purpose of the main factory is to allow you to include only a single module in all tests.
defmodule MyApp.Factory do use ExMachina.Ecto, repo: MyApp.Repo use MyApp.ArticleFactoryendThe main factory includes MyApp.ArticleFactory, so let’s create it next. It might be useful to create a separate directory for factories, like test/factories. Here is how to create a factory:
defmodule MyApp.ArticleFactory do defmacro __using__(_opts) do quote do def article_factory do %MyApp.Article{ title: "My awesome article!", body: "Still working on it!" } end end endendThis way you can split your giant factory file into many small files. But what about name conflicts? Use pattern matching to avoid them!
defmodule MyApp.PostFactory do defmacro __using__(_opts) do quote do def post_factory do %MyApp.Post{ body: "Example body" } end
def with_comments(%MyApp.Post{} = post) do insert_pair(:comment, post: post) post end end endend
# test/factories/video_factory.exdefmodule MyApp.VideoFactory do defmacro __using__(_opts) do quote do def video_factory do %MyApp.Video{ url: "example_url" } end
def with_comments(%MyApp.Video{} = video) do insert_pair(:comment, video: video) video end end endendIf you place your factories outside of test/support make sure they will compile by adding that directory to the compilation paths in your mix.exs file. For example for the test/factories files above you would modify your file like so:
... defp elixirc_paths(:test), do: ["lib", "test/factories", "test/support"]...Ecto Associations
Section titled “Ecto Associations”ExMachina will automatically save any associations when you call any of the
insert functions. This includes belongs_to and anything that is
inserted by Ecto when using Repo.insert!, such as has_many, has_one,
and embeds. Since we automatically save these records for you, we advise that
factory definitions only use build/2 when declaring associations, like so:
def article_factory do %Article{ title: "Use ExMachina!", # associations are inserted when you call `insert` comments: [build(:comment)], author: build(:user), }endUsing insert/2 in factory definitions may lead to performance issues and bugs,
as records will be saved unnecessarily.
Passing options to Repo.insert!/2
Section titled “Passing options to Repo.insert!/2”ExMachina.Ecto uses
Repo.insert!/2 to
insert records into the database. Sometimes you may want to pass options to deal
with multi-tenancy or return some values generated by the database. In those
cases, you can use c:ExMachina.Ecto.insert/3:
For example,
# return values from the databaseinsert(:user, [name: "Jane"], returning: true)
# use a different prefixinsert(:user, [name: "Jane"], prefix: "other_tenant")Flexible Factories with Pipes
Section titled “Flexible Factories with Pipes”def make_admin(user) do %{user | admin: true}end
def with_article(user) do insert(:article, user: user) userend
build(:user) |> make_admin |> insert |> with_articleUsing with Phoenix
Section titled “Using with Phoenix”If you want to keep the factories somewhere other than test/support,
change this line in mix.exs:
# Add the folder to the end of the list. In this case we're adding `test/factories`.defp elixirc_paths(:test), do: ["lib", "test/support", "test/factories"]Custom Strategies
Section titled “Custom Strategies”You can use ExMachina without Ecto, by using just the build functions, or you
can define one or more custom strategies to use in your factory. You can also
use custom strategies with Ecto. Here’s an example of a strategy for json
encoding your factories. See the docs on [ExMachina.Strategy] for more info.
[ExMachina.Strategy] !: https://hexdocs.pm/ex_machina/ExMachina.Strategy.html
defmodule MyApp.JsonEncodeStrategy do use ExMachina.Strategy, function_name: :json_encode
def handle_json_encode(record, _opts) do Poison.encode!(record) endend
defmodule MyApp.Factory do use ExMachina # Using this will add json_encode/2, json_encode_pair/2 and json_encode_list/3 use MyApp.JsonEncodeStrategy
def user_factory do %User{name: "John"} endend
# Will build and then return a JSON encoded version of the user.MyApp.Factory.json_encode(:user)Contributing
Section titled “Contributing”Before opening a pull request, please open an issue first.
git clone https://github.com/beam-community/ex_machina.gitcd ex_machinamix deps.getmix testOnce you’ve made your additions and mix test passes, go ahead and open a PR!
License
Section titled “License”ExMachina is Copyright © 2025 BEAM Community. It is free software, and may be redistributed under the terms specified in the [LICENSE]!(/LICENSE) file.