Chapter 5: Processes
Elixir code frequently runs many processes and a test author often wants to assert about the flow of messages between processes. Patch
provides some utilities that make listening to the messages between processes easy.
Listeners
Listeners are processes that sit between the sender process and the target process. The listener process will send a copy of every message to the test process so it can use ExUnit's built in assert_receive
, assert_received
, refute_receive
, and refute_received
functions.
Listeners are especially useful when working with named processes since they will automatically unregister the named process and take its place. For anonymous processes the inject/3
function is provided to assist in injecting listeners into other processes or the listener can be used in place of the target process when starting consumer processes.
Listeners are started with the listen/3
function and each have a tag
so that the test process can differentiate which listener has delivered which message.
defmodule PatchExample do
use ExUnit.Case
use Patch
test "sharded read replication" do
listen(:shard_a_leader, ShardALeader)
listen(:shard_a_replica_1, ShardAReplica1)
listen(:shard_a_replica_2, ShardAReplica2)
listen(:shard_b_leader, ShardBLeader)
listen(:shard_b_replica_1, ShardBReplica1)
listen(:shard_b_replica_2, ShardBReplica2)
send(ShardALeader, {:write, :some_value})
# Assert the leader gets the message
assert_receive {:shard_a_leader, {:write, :some_value}}
# Assert that the replicas for Shard A get the message too
assert_receive {:shard_a_replica_1, {:write, :some_value}}
assert_receive {:shard_a_replica_2, {:write, :some_value}}
# Assert that Shard A does not try to replicate to Shard B
refute_receive {:shard_b_leader, {:write, :some_value}}
refute_receive {:shard_b_replica_1, {:write, :some_value}}
refute_receive {:shard_b_replica_2, {:write, :some_value}}
end
end
GenServer Support
Listeners have special support for GenServers. By default a listener will provide the test process with all calls, replies, casts, and messages.
Given a listener with the tag :tag
the messages from a GenServer are formatted as follows.
Client Code | Message to Test Process |
---|---|
GenServer.call(pid, :message) | {:tag, {GenServer, :call, :message, from}} |
# if capture_replies = true | {:tag, {GenServer, :reply, result, from}} |
GenServer.cast(pid, :message) | {:tag, {GenServer, :cast, :message}} |
During a GenServer.call/3
the listener sits between the client and the server and reports back information to the test process.
.------------. .------. .--------. .------.
|Test Process| |client| |listener| |server|
'------------' '------' '--------' '------'
| | GenServer.call(message)| |
| | -----------------------> |
| | | |
| {GenServer, :call, message, from} | |
| <- - - - - - - - - - - - - - - - - - - - - - |
| | | |
| | | GenServer.call(message)|
| | | ----------------------->
| | | |
| | | reply |
| | | <-----------------------
| | | |
| {GenServer, :reply, reply, from} | |
| <- - - - - - - - - - - - - - - - - - - - - - |
| | | |
| | reply | |
| | <----------------------- |
.------------. .------. .--------. .------.
|Test Process| |client| |listener| |server|
'------------' '------' '--------' '------'`
GenServer.call/3
allows the client to set a timeout, an amount of time to wait for the server to response. The listener does not know how long the original client will wait for a timeout, the test author can provide a :timeout
option when spawning the listener to control how long the listener will wait for its GenServer.call/3
. By default the listener will wait 5000ms for each call, the default for GenServer.call/2
.
If the test doesn't require the listener to capture replies to GenServer.call
then the :capture_replies
option can be set to false. When this option is false the listener will simply forward the call onto the server. Refer to the following diagram for details on how this works.
.------------. .------. .--------. .------.
|Test Process| |client| |listener| |server|
'------------' '------' '--------' '------'
| | GenServer.call(message)| |
| | -----------------------> |
| | | |
| {GenServer, :call, message, from} | |
| <- - - - - - - - - - - - - - - - - - - - - - |
| | | |
| | | send(:"$gen_call", from, message)|
| | | --------------------------------->
| | | |
| | | reply |
| | <----------------------------------------------------------
.------------. .------. .--------. .------.
|Test Process| |client| |listener| |server|
'------------' '------' '--------' '------'
Target Monitoring
Listeners will automatically monitor the target process they are listening to. If the target process goes :DOWN
the listener will deliver a tagged {:DOWN, reason}
message to the test process and then exit.
Injecting
When working with processes in test code it is sometimes necessary to change the state of a running GenServer. Common use cases for injecting state into a GenServer are to set up some fixture data, update a configuration value, or replace a target pid with a listener from the previous section.
inject/3
is a helper that handles some common issues when updating state.
defmodule PatchExample do
use ExUnit.Case
use Patch
test "state can be updated" do
{:ok, pid} = Target.start_link(:initial_value)
assert :initial_value == Target.get_value(pid)
inject(pid, [:value], :updated_value)
assert :updated_value == Target.get_value(pid)
end
end
inject/3
accepts a GenServer.server
a list of keys
like one would use for put_in
and then a value to inject into the processes state.