Test factories
Many projects depend on external libraries to build their test data. Some of those libraries are called factories because they provide convenience functions for producing different groups of data. However, given Ecto is able to manage complex data trees, we can implement such functionality without relying on third-party projects.
To get started, let's create a file at "test/support/factory.ex" with the following contents:
defmodule MyApp.Factory do
alias MyApp.Repo
# Factories
def build(:post) do
%MyApp.Post{title: "hello world"}
end
def build(:comment) do
%MyApp.Comment{body: "good post"}
end
def build(:post_with_comments) do
%MyApp.Post{
title: "hello with comments",
comments: [
build(:comment, body: "first"),
build(:comment, body: "second")
]
}
end
def build(:user) do
%MyApp.User{
email: "hello#{System.unique_integer()}",
username: "hello#{System.unique_integer()}"
}
end
# Convenience API
def build(factory_name, attributes) do
factory_name |> build() |> struct!(attributes)
end
def insert!(factory_name, attributes \\ []) do
factory_name |> build(attributes) |> Repo.insert!()
end
end
Our factory module defines four "factories" as different clauses to the build function: :post
, :comment
, :post_with_comments
and :user
. Each clause defines structs with the fields that are required by the database. In certain cases, the generated struct also needs to generate unique fields, such as the user's email and username. We did so by calling Elixir's System.unique_integer()
- you could call System.unique_integer([:positive])
if you need a strictly positive number.
At the end, we defined two functions, build/2
and insert!/2
, which are conveniences for building structs with specific attributes and for inserting data directly in the repository respectively.
That's literally all that is necessary for building our factories. We are now ready to use them in our tests. First, open up your "mix.exs" and make sure the "test/support/factory.ex" file is compiled:
def project do
[...,
elixirc_paths: elixirc_paths(Mix.env),
...]
end
defp elixirc_paths(:test), do: ["lib", "test/support"]
defp elixirc_paths(_), do: ["lib"]
Now in any of the tests that need to generate data, we can import the MyApp.Factory
module and use its functions:
import MyApp.Factory
build(:post)
#=> %MyApp.Post{id: nil, title: "hello world", ...}
build(:post, title: "custom title")
#=> %MyApp.Post{id: nil, title: "custom title", ...}
insert!(:post, title: "custom title")
#=> %MyApp.Post{id: ..., title: "custom title"}
By building the functionality we need on top of Ecto capabilities, we are able to extend and improve our factories on whatever way we desire, without being constrained to third-party limitations.