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.

Run in Livebook

Stay up to date!

Get notified when I publish something new, and unsubscribe at any time.