# Split async and sync tests in one file

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.

```elixir
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, so `Sandbox.allow` for test A races with test B's concurrent allow on the same supervisor process
- `Ecto.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.Testing` with `testing: :inline` — jobs run in the calling process, inherit sandbox ownership, safe for async
- `Task.async/1` — inherits `$callers`, sandbox ownership propagates automatically
- `Req.Test.stub` — per-process by default, async-safe
- `Process.put` config 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.

[`ExUnit.Case` async option docs](https://hexdocs.pm/ex_unit/ExUnit.Case.html)


---

Created by: almirsarajcic
Date: April 29, 2026
URL: https://elixirdrops.net/d/TNYqKsbv
