How to rollback db changes between tests

I have multiple tests in one file. I want to create users at the beginning of each test and roll back those db changes at the end of each test.
At the top of my file I have this:

  use Teiserver.DataCase, async: true

which references this module

defmodule Teiserver.DataCase do


  use ExUnit.CaseTemplate

  using do
    quote do
      alias Teiserver.Repo

      import Ecto
      import Ecto.Changeset
      import Ecto.Query
      import Teiserver.DataCase
    end
  end

  @doc """
  Sets up the sandbox based on the test tags.
  """
  def setup_sandbox(tags) do
    pid = Ecto.Adapters.SQL.Sandbox.start_owner!(Teiserver.Repo, shared: not tags[:async])
    on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end)
  end

  setup tags do
    setup_sandbox(tags)
    :ok
  end

  @doc """
  A helper that transforms changeset errors into a map of messages.

      assert {:error, changeset} = Accounts.create_user(%{password: "short"})
      assert "password is too short" in errors_on(changeset).password
      assert %{password: ["password is too short"]} = errors_on(changeset)

  """
  def errors_on(changeset) do
    Ecto.Changeset.traverse_errors(changeset, fn {message, opts} ->
      Regex.replace(~r"%{(\w+)}", message, fn _, key ->
        opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string()
      end)
    end)
  end
end

However it seems like my changes are persisted between tests.

Also test_helper.exs has this

ExUnit.start()
Ecto.Adapters.SQL.Sandbox.mode(Teiserver.Repo, :manual)

alias Teiserver.Repo
alias Central.Helpers.GeneralTestLib

:ok = Ecto.Adapters.SQL.Sandbox.checkout(Repo, sandbox: false)

if not GeneralTestLib.seeded?() do
  GeneralTestLib.seed()
  Teiserver.TeiserverTestLib.seed()
end

Ecto.Adapters.SQL.Sandbox.checkin(Repo)

How do I make it so any db changes are rolled back after each test?

You can see the video in this link where I run the same test multiple times and it will just randomly fail eventually.

What’s wrong with using setup? It gets invoked before each test. And whatever you create in your each test should be automatically rolled back by Ecto at the end of each test. Have you tried?

As an experiment to maybe increase understanding, try putting that file in your project (f.ex. directly inside test/):

defmodule Stuff.AndStuffTest do
  setup do
    IO.puts("before each test")
    %{stuff: :you_need}
  end

  test "one thing", context do
    IO.inspect(context)
  end

  test "another thing", context do
    IO.inspect(context)
  end
end

…and run mix test test/stuff_and_stuff_test.exs, and see what output you are getting.

…Though you should probably not use sandbox manual mode. Any specific reason for doing that?

And don’t ever seed DB inside test_helper.exs. You should have a dedicated test seed task, and it’s very easy to make sure your test DB has certain contents at all times.

Alternatively, you can have a docker-compose.yaml file for it and you can put an initialization script of the containers which would load stuff into the DB after pg_ready returns an OK status.

So here is a sample test

defmodule Teiserver.Common.ClearDbEachTest do
  @moduledoc """
  Tests that the sandbox db is cleared after each test
  """
  use Teiserver.DataCase, async: true

  test "Sandbox test 1" do
    name = "ClearDbEachTestUser"
    user = Teiserver.TeiserverTestLib.new_user(name)
    result = Teiserver.CacheUser.get_user_by_id(user.id)
    assert result[:name] == name
  end

  test "Sandbox test 2" do
    name = "ClearDbEachTestUser"
    user = Teiserver.TeiserverTestLib.new_user(name)
    result = Teiserver.CacheUser.get_user_by_id(user.id)
    assert result[:name] == name
  end
end

Test 1 and 2 are identical. But Test 2 fails because the user created in Test 1 still exists when Test 2 is run. From my understanding, this shouldn’t happen since the sandbox changes should be cleared after each test.

This is a project that was inherited from someone who left. test_helper.exs can be modified if it’s wrong.

Have you tried on_exit/2?

Edit. I see you did that in your macro.