Finitomata ❤ Mox
· 3 min read
While pushing for FSMs in general and
Finitomata
in particular, I needed to provide a robust and handy way to test the beast. The ideology of
Finitomata
approach is to spawn a process per each entity alive, where
alive
means
in some intermediate state
. Generally speaking, that does not make the conventional testing a charm. Consider a process which might change its state (or, in terms of
Finitomata
,
can transition from one state to another
,) not only as a result of an explicit intervention, like a message sent to it, or, which is nearly the same, via
Finitomata.transition/4
, but also as a consequence or even a coincidence of some several external occasions.
Testing asynchronous interoperation is not the most trivial part of dealing with OTP, although we still might cook it right and make delicious. To simplify it and make testing pleasant and easy, I needed to inject some synchronization points, somewhat like mutexes , or, if you wish, erlang schedulers .
I always have been a big fan of callbacks. I tend to allow listener injection to each and every process I am implementing. If there are several phases of the process, I’d allow listening on phases change. If there is an input parsed, I’d welcome a listener on each line parsed. You got the point. FSM literally implies the existence of
on_transition
callback for some listener(s).
This listener would be an ideal
noun to mock
. During testing, this listener is to play the single role of
process synchronization guard
, but it must play it great. It can and should be used to synchronize control flow between testing and tested processes (and the whole outer world, if desired.) By introducing several synchronization points which are roughly speaking the callbacks themselves, it provides the catch-up scenario for the test code via
assert_receive/2
.
The main issue with implementing this idea was
Mox
demanded the process to call mocks to be started at the moment of mock declaration, which results is what I call
alive-lock
, as a contrary to notorious
dead-lock
from Java world. To declare mock, I needed a process and to test a process I needed the already declared mocks. To make my initial plan happen, I provided a pull request to
Mox
to allow deferred
pid
resolution, based on the process name. It has been merged and it might be tested right away with a dependency specified as
git: :master
.
So far so good, now I have a clean interface of any possible interop between testing process and the test itself.
Here is an example taken from
Finitomata
tests, which shows the approach.
Mox.defmock(Finitomata.Test.Listener, for: Finitomata.Listener)
defmodule Finitomata.Test.Mox do
@fsm """
idle --> |start!| started
started --> |do| done
"""
use Finitomata, fsm: @fsm, auto_terminate: true, listener: Finitomata.Test.Listener
…
end
This is the standard declaration of
Finitomata
worker:
FSM
, having three states, an unconditional transition to
started
and a normal transition to
done
. After
start!
event we might want to test initialization and after
do
we might want to test actual behaviour.
Here is the code in test doing exactly that, and I finally like how this code looks like.
parent = self()
Finitomata.Test.Listener
|> allow(parent, fn -> GenServer.whereis(fsm_name) end)
|> expect(:after_transition, 3, fn id, state, payload ->
parent |> send({:on_transition, id, state, payload}) |> then(fn _ -> :ok end)
end)
…
Finitomata.start_fsm(…)
…
# test payload after initialization
assert_receive {:on_transition, ^fsm_name, :idle, %{foo: ^bar}}
# test payload after unconditional state advance
assert_receive {:on_transition, ^fsm_name, :started, %{data: _}}
Finitomata.transition("MyFSM", {:do, event_payload})
# test payload after conditional transition
assert_receive {:on_transition, ^fsm_name, :done, %{outcome: _}}
Next step would be to export helper macros to decrease an amount of code needed to be written in each test.
Happy synchrotesting!