In our codebase, we have lots of tests that involve interacting with the database (via Postgrex). We have a handful of shared ExUnit.CaseTemplate
s 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 anon_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 theon_exit
handler has a chance to do its work.
- Doesn't fix the issue. Because
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
withmy_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.
- This too works, but it's quite clunky as well, since we'll have to replace every use of
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