🎓 A1Learner
🔍
0 of 46 lessons completed

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?