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?