< Logging | Up: README | Repo >
All test doubles are process-scoped. async: true tests run in full
isolation — each test process has its own doubles, state, and logs.
Task.async children
Task.async children automatically inherit their parent's doubles
via the $callers chain. No setup needed.
Explicit sharing with allow
Other processes (plain spawn, Agent, GenServer) need explicit
sharing:
DoubleDown.Double.allow(MyApp.Todos, self(), agent_pid)allow/3 also accepts a lazy pid function for processes that don't
exist yet at setup time:
DoubleDown.Double.allow(MyApp.Todos, fn -> GenServer.whereis(MyWorker) end)Global mode
For integration-style tests involving supervision trees, named GenServers, Broadway pipelines, or Oban workers — where individual process pids are not easily accessible — you can switch to global mode:
setup do
DoubleDown.Testing.set_mode_to_global()
DoubleDown.Double.fake(DoubleDown.Repo, DoubleDown.Repo.InMemory)
on_exit(fn -> DoubleDown.Testing.set_mode_to_private() end)
:ok
endIn global mode, all doubles registered by the test process are
visible to every process in the VM without explicit allow/3 calls.
Warning: Global mode is incompatible with async: true. When
active, all tests share the same doubles, so concurrent tests will
interfere with each other. Only use global mode in tests with
async: false. Call set_mode_to_private/0 in on_exit to restore
per-process isolation for subsequent tests.
Choosing the right approach
| Situation | Approach | async: true? |
|---|---|---|
| Direct function calls | No extra setup needed | Yes |
Task.async / Task.Supervisor | Automatic via $callers | Yes |
| Known pid (Agent, named GenServer) | allow/3 with the pid | Yes |
| Pid not known at setup time | allow/3 with lazy fn | Yes |
| Supervision tree / Broadway / Oban | set_mode_to_global/0 | No |
Example: testing a GenServer that dispatches through a contract
defmodule MyApp.WorkerTest do
use ExUnit.Case, async: true
setup do
MyApp.Todos
|> DoubleDown.Double.stub(:get_todo, fn [id] -> {:ok, %Todo{id: id}} end)
{:ok, pid} = MyApp.Worker.start_link([])
DoubleDown.Double.allow(MyApp.Todos, self(), pid)
%{worker: pid}
end
test "worker fetches todo via contract", %{worker: pid} do
assert {:ok, %Todo{id: "42"}} = MyApp.Worker.fetch(pid, "42")
end
endExample: testing through a supervision tree
When you can't easily get pids for every process in the tree, use global mode:
defmodule MyApp.PipelineIntegrationTest do
use ExUnit.Case, async: false
setup do
DoubleDown.Testing.set_mode_to_global()
DoubleDown.Double.fake(DoubleDown.Repo, DoubleDown.Repo.InMemory)
on_exit(fn -> DoubleDown.Testing.set_mode_to_private() end)
start_supervised!(MyApp.Pipeline)
:ok
end
test "pipeline processes events end-to-end" do
MyApp.Pipeline.enqueue(%{type: :invoice, amount: 100})
# ... assert on results ...
end
endCleanup
Call reset/0 to clear all doubles, state, and logs for the current
process:
setup do
DoubleDown.Testing.reset()
# ... set up fresh doubles ...
endIn practice, most tests just set doubles in setup without calling
reset — NimbleOwnership's per-process isolation means there's no
cross-test leakage.