1

In our codebase, we have lots of tests that involve interacting with the database (via Postgrex). We have a handful of shared ExUnit.CaseTemplates whose setup hook prepares the Ecto sandbox and such, and this works great.

The problem I'm running into is that processes spawned by our tests may still be communicating with the database when the test process exits, so we get errors in the logs that look like this:

09:56:51.517 [error] Postgrex.Protocol (#PID<0.2127.0>) disconnected: ** (DBConnection.ConnectionError) owner #PID<0.11626.0> exited

(The "owner" PID here is the test process, which of course spawned the database connection in its setup.)

Things I've tried:

  • In the shared setup hook, adding an on_exit/2 handler that will clean up the database processes.

    • Doesn't fix the issue. Because on_exit runs in a separate process after the test process dies, there's a race condition here that manifests maybe 5% of the time: Postgrex can try to communicate with the now-dead test process before the on_exit handler has a chance to do its work.
  • As as the last line of every test, explicitly call a shared cleanup function to wait on the database transactions to finish.

    • This works, but it's cumbersome, and quite easy to forget (both in development and PR review) when you have hundreds of tests. It's been a continual source of test flakiness for us because of this.
    • Worse, because the error messages occur asynchronously with the test, it's a pain to track down which test is missing the cleanup—you'll get the error some time after the offending test completes, but who knows how long after!
  • Copy & paste the test macro from the ExUnit source into our shared test code, with the one change being that it appends the call to our cleanup function to the end of test body.

    • This too works, but it's quite clunky as well, since we'll have to replace every use of test with my_test or whatever, and we'll be stuck maintaining the copypasta.
    • For resiliency, we could probably wrap the whole test body in a try/catch block and call the cleanup function regardless of how the test process exits.

I gather from spelunking in the Elixir core mailing list that before on_exit/2 existed, there was once a teardown hook that ran synchronously at the end of the test process. I'd really appreciate any solution that could imitate such functionality.

Edited to add: Here's a sample of what our shared ExUnit.CaseTemplate looks like:

defmodule PersistenceTestCase do
  use ExUnit.CaseTemplate

  setup tags do
    # This line would be in test_helper.exs, but it breaks tests not using
    # this test case that have implicit data-persistence side-effects
    Ecto.Adapters.SQL.Sandbox.mode(App.Repo, :manual)

    :ok = Ecto.Adapters.SQL.Sandbox.checkout(App.Repo)

    unless tags[:async] do
      Ecto.Adapters.SQL.Sandbox.mode(App.Repo, {:shared, self()})
    end

    on_exit(fn ->
      # Reset on exit to not interfere with tests not using this test case
      Ecto.Adapters.SQL.Sandbox.mode(App.Repo, :auto)
    end)
  end
end
s3cur3
  • 2,749
  • 2
  • 27
  • 42

0 Answers0