We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
Split async and sync tests in one file
almirsarajcic
Marking a test module async: false serializes every test inside it. If only two tests genuinely need it — because they touch a globally-named Task.Supervisor or a singleton GenServer — the other thirty tests pay the serial cost for no reason.
Two defmodule blocks in one file fixes this. ExUnit discovers and runs them as independent modules: the async: true module runs in parallel with everything else; the async: false module runs serially on its own. Only the genuinely-blocking tests pay the cost.
defmodule MyAppWeb.WebhookControllerTest do
use MyAppWeb.ConnCase, async: true
describe "GET /webhook — verification" do
test "returns challenge for valid request", %{conn: conn} do
# Safe async: no task spawned, assertion is on HTTP response only
end
test "rejects missing token", %{conn: conn} do
# Safe async: validation happens in the request process
end
end
end
defmodule MyAppWeb.WebhookControllerSyncTest do
# async: false — POST handler spawns via MyApp.TaskSupervisor (globally named).
# The task writes to DB; per-test Sandbox.allow races under concurrency.
use MyAppWeb.ConnCase, async: false
describe "POST /webhook — message handling" do
test "persists incoming message", %{conn: conn} do
post(conn, ~p"/webhook", valid_payload())
# Wait for the spawned task to complete
Process.sleep(100)
assert Repo.aggregate(Message, :count) == 1
end
end
end
The blocker comment on *SyncTest is mandatory. Without it the next developer cleaning up test performance will flip it to async: true, the flake returns, and nobody knows why. The comment must name the specific global resource and explain the mechanism — not just say “async: false for safety”.
What forces a SyncTest split:
Task.Supervisor.start_child(GloballyNamedSup, fn -> Repo.query... end)— the supervisor is shared, soSandbox.allowfor test A races with test B’s concurrent allow on the same supervisor processEcto.Adapters.SQL.Sandbox.mode(Repo, :shared)in setup — shared mode disables per-test isolation- Singleton GenServer state (
MyApp.Cache,MyApp.RateLimiter) that tests mutate without a reset hook
What does NOT need a split:
Oban.Testingwithtesting: :inline— jobs run in the calling process, inherit sandbox ownership, safe for asyncTask.async/1— inherits$callers, sandbox ownership propagates automaticallyReq.Test.stub— per-process by default, async-safeProcess.putconfig overrides — discarded per test automatically
Name the sync module <ModuleName>SyncTest and keep it in the same file as <ModuleName>Test. A separate _sync_test.exs file breaks the connection between the two and makes the blocker comment hard to find.
copied to clipboard