Elixir Supervised Tests
Supervised tests start a process under the test supervisor, so it is stopped when the test ends. Each test gets a fresh process, with no manual cleanup.
start_supervised starts a fresh child for one test
A supervised counter starts at the given value. Incrementing it changes only that child.
ExUnit.start(seed: 0, auto_run: false, formatters: [])
defmodule Counter do
use GenServer
def start_link(n), do: GenServer.start_link(__MODULE__, n, [])
def init(n), do: {:ok, n}
def handle_call(:get, _from, n), do: {:reply, n, n}
def handle_cast(:inc, n), do: {:noreply, n + 1}
end
defmodule CounterTest do
use ExUnit.Case
test "supervised counter starts fresh" do
{:ok, pid} = start_supervised({Counter, 5})
assert GenServer.call(pid, :get) == 5
GenServer.cast(pid, :inc)
assert GenServer.call(pid, :get) == 6
end
end
result = ExUnit.run()
IO.puts("passed: #{result.total - result.failures}/#{result.total}")Two tests get two separate children
Each test starts its own child. The two tests see different starting values.
ExUnit.start(seed: 0, auto_run: false, formatters: [])
defmodule Counter do
use GenServer
def start_link(n), do: GenServer.start_link(__MODULE__, n, [])
def init(n), do: {:ok, n}
def handle_call(:get, _from, n), do: {:reply, n, n}
def handle_cast(:inc, n), do: {:noreply, n + 1}
end
defmodule SeparateTest do
use ExUnit.Case
test "first test sees five" do
{:ok, pid} = start_supervised({Counter, 5})
assert GenServer.call(pid, :get) == 5
end
test "second test sees nine" do
{:ok, pid} = start_supervised({Counter, 9})
assert GenServer.call(pid, :get) == 9
end
end
result = ExUnit.run()
IO.puts("passed: #{result.total - result.failures}/#{result.total}")Setup can start the child for every test
Setup runs before each test. Each test still receives a fresh child.
ExUnit.start(seed: 0, auto_run: false, formatters: [])
defmodule Counter do
use GenServer
def start_link(n), do: GenServer.start_link(__MODULE__, n, [])
def init(n), do: {:ok, n}
def handle_call(:get, _from, n), do: {:reply, n, n}
def handle_cast(:inc, n), do: {:noreply, n + 1}
end
defmodule CounterSetupTest do
use ExUnit.Case
setup do
{:ok, pid} = start_supervised({Counter, 0})
{:ok, counter: pid}
end
test "increment from setup", %{counter: pid} do
GenServer.cast(pid, :inc)
assert GenServer.call(pid, :get) == 1
end
test "fresh counter from setup", %{counter: pid} do
assert GenServer.call(pid, :get) == 0
end
end
result = ExUnit.run()
IO.puts("passed: #{result.total - result.failures}/#{result.total}")Example
ExUnit.start(seed: 0, auto_run: false, formatters: [])
defmodule Counter do
use GenServer
def start_link(n), do: GenServer.start_link(__MODULE__, n, [])
def init(n), do: {:ok, n}
def handle_call(:get, _from, n), do: {:reply, n, n}
def handle_cast(:inc, n), do: {:noreply, n + 1}
end
defmodule CombinedCounterTest do
use ExUnit.Case
setup do
{:ok, pid} = start_supervised({Counter, 10})
{:ok, counter: pid}
end
test "setup child increments", %{counter: pid} do
GenServer.cast(pid, :inc)
assert GenServer.call(pid, :get) == 11
end
test "a second child starts apart", %{counter: pid} do
{:ok, other} = start_supervised(Supervisor.child_spec({Counter, 100}, id: :second_counter))
assert GenServer.call(other, :get) == 100
assert GenServer.call(pid, :get) == 10
end
end
result = ExUnit.run()
IO.puts("passed: #{result.total - result.failures}/#{result.total}")Key points
- start_supervised: a function that starts a child under the test supervisor and stops it when the test ends.
- Test supervisor: the supervisor ExUnit runs for each test, owning children started inside that test.
- Fresh state: a new child for each test, so one test cannot leak state into the next test.
- Child spec: the module and argument pair that tells the supervisor how to start the child.
- Setup: a block that runs before each test and can start a child for that test.
In this example, setup starts a counter at 10 for each test. The first test increments the setup counter to 11. The second test starts a second counter at 100 with a distinct id and checks that the setup counter is still 10.
Note
call start_supervised inside a test or inside setup, because only a running test owns a test supervisor.
Try it Yourself
Edit the code, then press Run.
Running is simulated
Exercise
What happens to a child started with start_supervised when its test ends?