🎓 A1Learner
🔍
0 of 46 lessons completed

Elixir Testing GenServers

GenServer testing is checking a running server through its public calls and the messages it sends. The test starts the server, drives it, and stops it again.

A call, a cast, and a message together

The test starts the server under supervision and checks its starting count. A cast raises the count and sends a message, which the test receives and then confirms with a call.

ExUnit.start(seed: 0, auto_run: false, formatters: [])
defmodule Counter do
  use GenServer

  def start_link(listener) do
    GenServer.start_link(__MODULE__, listener, name: __MODULE__)
  end

  def child_spec(listener) do
    %{id: __MODULE__, start: {__MODULE__, :start_link, [listener]}}
  end

  def init(listener), do: {:ok, %{count: 0, listener: listener}}

  def handle_call(:get, _from, state), do: {:reply, state.count, state}

  def handle_cast(:inc, state) do
    next = state.count + 1
    send(state.listener, {:counted, next})
    {:noreply, %{state | count: next}}
  end
end
defmodule CounterTest do
  use ExUnit.Case

  test "counts and notifies" do
    {:ok, _pid} = start_supervised({Counter, self()})
    assert GenServer.call(Counter, :get) == 0
    GenServer.cast(Counter, :inc)
    assert_receive {:counted, 1}
    assert GenServer.call(Counter, :get) == 1
    GenServer.stop(Counter)
  end
end
result = ExUnit.run()
IO.puts("passed: #{result.total - result.failures}/#{result.total}")

A later call proves casts were applied

Two casts arrive before the call, and messages from one sender keep their order. The call answer proves both casts were applied, and the server is stopped at the end.

ExUnit.start(seed: 0, auto_run: false, formatters: [])
defmodule Counter do
  use GenServer

  def start_link(listener) do
    GenServer.start_link(__MODULE__, listener, name: __MODULE__)
  end

  def child_spec(listener) do
    %{id: __MODULE__, start: {__MODULE__, :start_link, [listener]}}
  end

  def init(listener), do: {:ok, %{count: 0, listener: listener}}

  def handle_call(:get, _from, state), do: {:reply, state.count, state}

  def handle_cast(:inc, state) do
    next = state.count + 1
    send(state.listener, {:counted, next})
    {:noreply, %{state | count: next}}
  end
end
defmodule CounterTest do
  use ExUnit.Case

  test "counts two casts" do
    {:ok, _pid} = start_supervised({Counter, self()})
    GenServer.cast(Counter, :inc)
    GenServer.cast(Counter, :inc)
    assert GenServer.call(Counter, :get) == 2
    GenServer.stop(Counter)
  end
end
result = ExUnit.run()
IO.puts("passed: #{result.total - result.failures}/#{result.total}")

A call can both answer and send a message

The call returns the doubled number at once. The server also sends the same answer as a message, which the test checks before stopping the server.

ExUnit.start(seed: 0, auto_run: false, formatters: [])
defmodule Doubler do
  use GenServer

  def start_link(listener) do
    GenServer.start_link(__MODULE__, listener, name: __MODULE__)
  end

  def child_spec(listener) do
    %{id: __MODULE__, start: {__MODULE__, :start_link, [listener]}}
  end

  def init(listener), do: {:ok, %{listener: listener}}

  def handle_call({:times_two, n}, _from, state) do
    answer = n * 2
    send(state.listener, {:times_two, answer})
    {:reply, answer, state}
  end
end
defmodule DoublerTest do
  use ExUnit.Case

  test "answers and notifies" do
    {:ok, _pid} = start_supervised({Doubler, self()})
    assert GenServer.call(Doubler, {:times_two, 21}) == 42
    assert_receive {:times_two, 42}
    GenServer.stop(Doubler)
  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(listener) do
    GenServer.start_link(__MODULE__, listener, name: __MODULE__)
  end

  def child_spec(listener) do
    %{id: __MODULE__, start: {__MODULE__, :start_link, [listener]}}
  end

  def init(listener), do: {:ok, %{count: 0, listener: listener}}

  def handle_call(:get, _from, state), do: {:reply, state.count, state}

  def handle_cast(:inc, state) do
    next = state.count + 1
    send(state.listener, {:counted, next})
    {:noreply, %{state | count: next}}
  end
end
defmodule CounterTest do
  use ExUnit.Case

  test "counts once" do
    {:ok, _pid} = start_supervised({Counter, self()})
    GenServer.cast(Counter, :inc)
    assert_receive {:counted, 1}
    assert GenServer.call(Counter, :get) == 1
    GenServer.stop(Counter)
  end

  test "counts twice from a fresh start" do
    {:ok, _pid} = start_supervised({Counter, self()})
    GenServer.cast(Counter, :inc)
    GenServer.cast(Counter, :inc)
    assert_receive {:counted, 1}
    assert GenServer.call(Counter, :get) == 2
    GenServer.stop(Counter)
  end
end
result = ExUnit.run()
IO.puts("passed: #{result.total - result.failures}/#{result.total}")

Key points

  • Supervised start: start_supervised starts the server under the test, so it is cleaned up with the test.
  • Call: GenServer.call asks the server a question and waits for its answer.
  • Cast: GenServer.cast sends a change without waiting for an answer.
  • Message: a server can send a message to the test process, which assert_receive then checks.
  • Stop: the test stops the server with GenServer.stop before the test ends.

In this example, each test starts its own supervised counter, drives it with casts, checks the message it sends, and confirms the count with a call before stopping the server. The second test starts from a fresh count of zero, and both tests pass.

Note

a cast is checked through the message it causes or a later call, never by its own return value.

Try it Yourself

Edit the code, then press Run.

Running is simulated

Exercise

How does a test check a message that a server sends to the test process?