Factories with ExMachina - Avoid duplicated records
A typical situation we face when testing Apps is creating data for relationships. We can do this in a few ways, but a common one is to insert the “parent” record and later use it when inserting the child ones.
user = Factory.insert(:user, name: "John Doe")
account = Factory.insert(:account, user: user)
After creating the records, you can call your functions and assert you get the expected result:
assert {:ok, %{status: "active"}} = Accounts.activate_account(account)
The factories to make this possible would look something like this:
defmodule MyApp.Factory do
use ExMachina.Ecto, repo: MyApp.Repo
def user_factory do
%MyApp.Users{
name: "Jane Smith",
email: sequence(:email, &"email-#{&1}@example.com")
}
end
def account_factory do
%MyApp.Accounts{
user: insert(:user),
provider: "email",
status: "created"
}
end
end
As you can see, the account_factory
does an insert(:user)
as it is a required relation. Typically, this is not a problem because you will use the account
and user
to call a function and assert the result.
But using insert
will cause two main problems. First, you will generate an extra user every time you create an account. Exmachina will call Repo.insert
on every call to insert
. This will accumulate, and eventually, you can start having performance issues (the test suite getting slower and slower).
The second one is flaky tests caused by the extra record. If your test depends on creating a specific number of records, it will fail, and the reason will not be clear.
user = Factory.insert(:user, name: "John Doe")
account = Factory.insert(:account, user: user)
assert Account |> Repo.aggregate(:count, :id) == 1 #> :ok
assert User |> Repo.aggregate(:count, :id) == 1 #> :error
Luckily, the fix is super simple. Change your factory to build
instead of insert
.
def account_factory do
%MyApp.Accounts{
user: build(:user),
provider: "email",
status: "created"
}
end
user = Factory.insert(:user, name: "John Doe")
account = Factory.insert(:account, user: user)
assert Account |> Repo.aggregate(:count, :id) == 1 #> :ok
assert User |> Repo.aggregate(:count, :id) == 1 #> :ok
Your tests will continue to work, but now you don’t waste resources and avoid strange failing tests.