Implementing a test data sequencer using Agent

I’ve been trying to learn more about GenServer and friends, and getting increasingly obsessive about keeping dependencies as minimal as possible (the ease of which is one of my “new” favorite things about working with Elixir). So when the need arose to generate some unique test data in a project, instead of adding ExMachina (which I have found not nearly as helpful as FactoryBot was in Rails projects), I decided to see what I could do with an Agent.

The code to handle the sequencing was pretty trivial:

defmodule Sequencer do
  use Agent

  def start_link(_) do
    Agent.start_link(fn -> %{} end, name: __MODULE__)

  def get(id) do
    Agent.get_and_update(__MODULE__, fn state ->
      {state, Map.update(state, id, 0, fn seq -> seq + 1 end)}

    Agent.get(__MODULE__, & &1)
    |> Map.fetch!(id)

And then to use in my test (after adding to supervision tree):

Repo.insert!(%User{email: "test-#{Sequencer.get(:user_email)}"})

This seems to work, but I haven’t tested very intensely at all. I feel like I must be missing some pitfalls with this approach…

From what I understand, calls to Sequencer will always be processed one at a time, so there’s no chance of a collision as long as the name is unique.

Combining the two Agent operations in a single method was the only vaguely smelly thing, but even there I’m not aware of any problems with that.

Please roast my code.

This is totally fine, although for your purposes I think you could probably just use :erlang.unque_integer([:positive, :monotonic]).

Your overall approach of avoiding ExMachina is a good one, the next step however in my books is to, when possible, avoid using Repo to setup test data, and instead use your context functions. It can be extremely easy to write tests that create data that is nice and perfect for the test because it is being created directly, but doesn’t match how data in your system is actually created.

The other issue with direct Repo usage is that it can also fail to create enough data, when your create flows also create secondary important records in other tables. Where possible, just use your context functions.


Whoa, cool.

avoid using Repo to setup test data, and instead use your context functions

That makes some sense. I’ll try it out.


1 Like