In this tutorial, we will use PropEr to test a group of interacting processes.
The system under test consists of one master and multiple slave processes. The
main concept is that the master plays ping-pong (i.e. exchanges ping and pong
messages) with all slave processes, which do not interact with each other. For
the rest of this tutorial, we will refer to the slave processes as the
ping-pong players.
The ping-pong master
The ping-pong master is implemented as an Erlang gen_server. The internal
state of the server is a dictionary containing the scores (i.e. the number
of ping-pong message exchanges with the master) of all ping-pong players.
External clients can make the following requests:
start and link to the ping-pong master
stop the ping-pong master
add a new ping-pong player to interact with the master
remove a ping-pong player
send a ping message to the server
get the score of a given player
In order to test the stand-alone behaviour of the ping-pong master we can
define an abstract state machine, as described in
this tutorial about testing generic
servers with PropEr. The state machine specification for the ping-pong master
can be found here.
The ping-pong players
A ping-pong player is a process spawned and registered as Name that executes
the following loop:
When a player is asked by an external client to play ping-pong, she will send a
ping message to the ping-pong master. On the other hand, if asked to play
tennis or football, the player replies with a message expressing her dislike for
any sport other than ping-pong. The API for interacting with a ping-pong player
is the following:
It’s ping-pong time!
It’s now time to test that the system behaves as expected when the ping-pong
players interact with the master. To this end, we will specify an abstract
state machine modeling the master’s internal state, just as we would do to
test the stand-alone behaviour of the master. We choose to base our
state machine specification on the master process because this is the main
component of the system under test. But now, instead of making ping/1 calls
directly to the master, we will instruct the ping-pong players to do so by
performing the asynchronous play_ping_pong/1 call. Moreover, we will include
synchronous play_tennis/1 calls to the ping-pong players, to test that such
calls do not influence the players’ interaction with the master. In our case,
this is quite obvious. But when testing, for example, the interaction of
processes in a big supervision tree, we cannot be sure about the possible
side-effects of each operation.
On the other hand, it is important to keep the complexity of our model at a
reasonable level. Otherwise, it’s quite probable to make errors in the
state machine specification. For each different feature we would like to test,
defining a simple state machine that concentrates on the operations related to
that feature will usually reveal any inconsistencies between the model and the
actual system behaviour. These inconsistencies will be reflected in the results
of the selected API calls.
Below we give the abstract state machine that will be used to test the ping-pong
system. As usual, it specifies:
The initial state of the model:
The API calls that will be tested:
State updates:
Preconditions that should always be respected (even while shrinking):
And finally, postconditions about the results of the calls:
Having successfully tested the stand-alone behaviour of the master, we expect
this property to pass the tests:
But…
…the property fails, along with error reports on the server crashing!
What is more, the History and State fields contain dictionaries which are
printed out based on their internal representation. We decide to deal with this
issue by including some pretty-printing functions in the property, so as to
output more informative debugging information.
And run the test once more:
Of course the property still fails and new error reports are produced.
This happens because the asynchronous play_ping_pong/1 operation introduces
non-determinism in the order in which messages are received by the server. Here
we can see yet another benefit of property based testing: it helps to increase
our understanding about process interaction in the system under test.
Fixing the postcondition of get_score/1 so as to achieve deterministic
results is quite simple in this case:
The error reports, however, are triggered by a not-so-evident bug in the code.
They are occassionaly produced when stopping the server, because of an attempt
to get and subsequently kill the pid associated with a name that is actually
not present in the process registry. Let us re-examine the code that’s
executed when stopping the server:
The exception raised suggests that there exist some names which are stored in
the server’s internal dictionary, but are not associated with any (process) pid.
But where do these names come from? To get the answer we have to take a look at
how ping messages are handled by the server:
This suggests that incoming ping messages associated with names not present
in the server’s dictionary are actually inserted in the dictionary. When we
perform an asynchronous play_ping_pong/1 request to a player, there is a
chance that this player might be removed before her ping message is received
by the master. In this case, when the master eventually receives the ping
message, the name of the removed player will be added to the dictionary,
despite not being associated with any process. Having spotted the bug, we
can easily fix it:
And now the property successfully passes many tests: