Dezyne: Implementing and Testing Components
Introduction
In this article, I will present an implementation for a motor in Dezyne. The implementation will build upon the MotorGlue
introduced in a previous article, incorporating error handling and an asynchronous protocol. Firstly, I will introduce the necessary interfaces for composing the desired behavior and present the Motor
interface. Subsequently, I will provide an implementation for the Motor
interface, referred to as MotorImpl
. Following that, I will discuss the aspects that require testing and those that are already handled by the Dezyne verification. I will then demonstrate how to create mocks for the Dezyne interfaces. Finally, I will create fixtures and tests to assess the behavior of the MotorImpl
component.
All examples in this article are based on Dezyne version 2.18.
Let’s begin by introducing the required interfaces.
The Interfaces
The MotorImpl
component will provide the Motor
interface and require the following interfaces:
- Config
- Timer
- Async
- MotorGlue
Motor Interface
The Motor
interface follows a strict design to enable proper verification of its usage in a design by contract fashion. The Initialize and Terminate functions serve as the constructor and destructor, respectively. Prior to usage, the motor must transition to the Operational state. Starting the motor will home the motor axis. Once the motor is in the operational state, the Move
command can be used to initiate an asynchronous movement, while the StopMoving
command can be used to halt the motor’s movement. In the event of an error, the motor will transition to an error state. While in the error state, the motor cannot be used until it is recovered and restarted. To maintain conciseness, certain details have been omitted from the example.
|
|
Config Interface
The Config
interface provides access to the motor’s configuration. It includes two in-events that can be used to query the home and move timeout for the motor with the specified id
.
|
|
The interface can be implemented by utilizing a foreign component that has access to the configuration file and exposes its content accordingly. The configuration file could have a structure similar to the following example:
{
"motor": {
"xaxis": {
"home_timeout": 5.0,
"move_timeout": 3.0,
},
"yaxis": {
"home_timeout": 10.0,
"move_timeout": 6.0,
}
}
}
Timer Interface
The Timer
interface allows for managing timeouts. This interface can be implemented by utilizing a foreign component.
|
|
Async Interface
The Async
interface is as described in the earlier Async post about asynchronous operations.
|
|
MotorGlue Interface
The MotorGlue
interface was introduced in the previous post about testing glue.
|
|
Implementing the MotorImpl Component
By utilizing these interfaces, we can define the complete behavior of the motor. Let’s start by defining all the provided and required ports. Additionally, the Config
interface is injected, meaning that it should be made available through the locator.
|
|
The behavior begins with defining the State
enum. It is important to note that the state of this component may not always match the state of the interface. Therefore, we need to track the component’s state separately and cannot rely on shared state for this purpose. Additionally, the homeTimeout
and moveTimeout
are two data variables that will be used to store the timeout configuration values.
|
|
Dezyne does not currently support early returns (early replies) for event blocks. To avoid excessive indentation levels and maintain code readability, I follow a coding style similar to the example shown below in the Initialize
function. This approach allows for a more concise and readable code structure, even without deep indentation.
|
|
Early return is supported for functions in Dezyne. In this case, a Start
function has been defined, which will be utilized later in the Start in-event. When starting the motor, it needs to perform the homing process. This involves stopping the motor and then calling the Home
method on the glue. Immediately after that, we check if the homing process has already completed. If it has, it means the homing was very quick, and we can send an OnOk
out-event. Otherwise, we start a timeout timer and transition to the Starting state to monitor the OnHomingChanged
events and wait for the homing process to finish.
Since Start
is an asynchronous operation as specified in the interface, we cannot directly send an OnOk
or OnError
event here. We need to decouple the event. This can be done by using the defer
keyword, but for better testability I choose to utilize the Async
interface as explained in the Async article.
|
|
Here we utilize the Start
function for the Start
in-event.
|
|
In the IdleError
state, we encounter a situation where the interface does not define the Start
in-event. However, in this state, it becomes necessary to implement the Start
in-event. This occurs when the component is in the IdleError
state, but the interface still assumes it is in the Idle
state. This mismatch can arise when the Stop
command is sent after an error is detected but not yet reported to the client.
Here we see that the client and component are not synchronized anymore. The api
is in the state State:Idle
while MotorImpl
is in the state State::IdleError
.
To address this, the component needs to notify the client that it is in the IdleError
state. When the client calls the Start
in-event the component will always reply with an OnError
out-event. This process effectively synchronizes the client and the component, ensuring both are aware of the current state.
In the Starting
state, the component actively monitors the OnHomingChanged
signal. Upon receiving this signal, it checks whether the motor has completed the homing process. If the homing process is complete, the component sends an OnOk
out-event. However, if the motor fails to finish homing within the specified timeout, the timer triggers an OnTimeout
out-event. This event is utilized to send an OnError
out-event and transition the component to the IdleError state.
The inclusion of the timer ensures compliance with the interface requirements. Although the OnHomingChanged
events are optional and may or may not occur, the timer guarantees the occurrence of an OnTimeout
event, enabling the component to fulfill the interface specifications.
|
|
The Starting_SendOk
and Starting_SendError
states are used to comply to the asynchronous reply to the Start
command when needed.
|
|
In the Operational
state, we define the Move
function, which follows a structure similar to the Start
function. In this case, since we are in the Operational
state, if something goes wrong during the movement, we will transition to the OperationalError
state.
|
|
The implementation of the Operational
and OperationalError
states follows the same concepts and structures that we have already discussed. There are no new or surprising elements introduced in these states.
|
|
During the movement process, the OnMovingChanged
out-event is monitored to determine when the move is finished. This allows the component to track the progress of the movement. Additionally, a timer is utilized to ensure compliance with the interface requirements.
|
|
And lastly the Moving_SendOk
and Moving_SendError
states are used to asynchronously reply to the Move
command when needed.
|
|
About Verification and Unit Testing
For interfaces the following verification checks are done:
- No deadlocks - the interface must always be able to make progress.
- No unreachable code - the interface does not contain any unreachable code.
- No livelocks - the client always has an opportunity to interact with the interface.
- Deterministic - all state changes in the interface can be determined by the in- and out-events and reply-values.
For components the verification checks are:
- Deterministic - for each state no more than one event handler per event can be defined.
- No Illegals - the component never tries to execute an illegal.
- No deadlocks - the component can always make progress.
- No unreachable code - the component does not contain any unreachable code.
- No livelocks - the component never gets into a loop in which it is always busy.
- Compliant - the component always follows the contract of the interfaces.
With all these checks in place a lot of things are covered and do not have to be unit tested. The resulting component will be responsive, and all events can be handled. The design of the interfaces are also important. By making an interface restrictive you can ensure the order of calls. For example, by forcing the call to Initialize
before allowing any other in-events, you know all components will be initialized before use. Terminate
will go back to the state Uninitialized
, and therefore you will find out if you forget to terminate a component, because a not terminated component cannot be initialized again. With verification this becomes immensely powerful, because it will check all code paths, which is often not feasible with unit tests.
The usage of the component can be effectively managed by designing strict interfaces and leveraging verification to check correct usage. However, there are also some things that will not be covered by verification and will have to be tested using unit tests.
- Data is outside the scope of Dezyne. All data handling and passing will have to be unit tested.
- Functional behavior will have to be unit tested. For example, in our motor, before the homing is started, the motor must be stopped.
In our example, additional unit testing is required because the MotorGlue
interface is primarily a permissive interface that does not provide guidance on how to use it. The MotorImpl
component, therefore, needs to encapsulate the knowledge of how to utilize the MotorGlue
interface effectively. The implementation of the usage rules will be coded within the MotorImpl
component and will need to be thoroughly unit tested to ensure their correctness and adherence to the desired behavior.
The developers of Dezyne are working on functional testing for Dezyne. When functional testing becomes available, unit testing will primarily be focused on data handling and other handwritten code.
For the MotorImpl
we will not test:
- If there are any dead locks or life locks.
- If we follow the contracts of the interfaces.
- That the component needs to be initialized before started.
- That the component needs to be started before moving.
- That movements can be interrupted.
Because all of that is handled by verification. But we will test that:
- Initializing will get the right values from the configuration.
- Before the homing, the motor will be stopped.
- That the homing and moving timeouts use the right value from the configuration.
- That the component correctly detects the end of the movement.
- That the component can handle when the movement or homing is directly finished.
- That the component will go into error after a timeout.
- That stop will be called on the motor when the component goes into error.
- That stop and stop moving will call stop on the motor.
Testing
About the dzn::pump
The MotorImpl
does not use the blocking and defer features of Dezyne, meaning the Dezyne pump is not used. Therefore, we can use the main thread to run the tests on. This way we know there is never an event waiting in the queue (pump) and we can call in- and out-events directly. This makes the tests more direct.
You will notice when the pump is needed. When it cannot be found in the dzn::locator
, an exception will be thrown.
Making Mocks for Dezyne Interfaces
By looking at the generated code of the MotorImpl
in MotorImpl.hh
and MotorImpl.cc
we can extract what is needed to manually create a component. This does involve some boilerplate code. Using this, a mock for the Timer
interface can be constructed.
|
|
The idea for the mock is that for each in-event we create a matching mock method. Then we connect the in-event to this mock method. Now we can set expectations on the calls to the in-events. Optionally some helper methods could be added for sending out-events. But because we are making the tests single-threaded without using the pump, out-events can be called directly when needed.
We have to reuse this pattern for the Async
, Config
and MotorGlue
mocks. To reduce the amount of boilerplate code I propose to create the following helper classes:
|
|
Using these helper classes the TimerMock
can be written as follows:
|
|
The AsyncMock
, MotorGlueMock
and ConfigMock
follow the same pattern. The last thing we need is a mock to track the out-events of the MotorImpl
component.
|
|
The MotorImplTest
Fixture
Now that we have all the mocks, we can create the fixture. The fixture will set up the Dezyne environment, instantiate all the objects and connect everything together.
A Dezyne environment will always consist of a dzn::locator
and a dzn::runtime
. The runtime must be given to the locator. Often also a dzn::pump
will be given to the locator, but as discussed above for these tests we do not need the pump. The glue
, timer
, async
, config
and events
member variables are declared as strict mocks. All Dezyne components are initialized by giving it the locator. Because config
is declared as required injected Config config
in the MotorImpl
component, it will be retrieved from the locator. That is why we added it to the locator here.
In the fixture’s constructor, the components are connected together. The out-events are connected to the MotorImplEventsMock
. The last step is to check if everything has been fully connected by calling dzn::check_bindings
on every component.
|
|
Testing Initialize
For initialize we test if the motorId
is used for the calls to config
and that the glue
is initialized using the same motorId
.
|
|
The failure cases for Initialize
are not tested here. The verification covers this enough. The happy flow of this test together with other tests and the verification will cover all fail cases. The same is true for the call to Terminate
.
Testing Start
MotorImplTest_Idle
Fixture
To test Start
a fixture representing the Idle
state is created. The fixture will set the two timeouts to known values and initialize the glue.
|
|
Start
Tests
This first test is the happy flow for the homing using the following scenario. After sending the home command, it is expected that IsHoming
returns true. Because the state of homing changed, an OnHomingChanged
event is expected for which the IsHoming
still returns true. Sometime later the OnHomingChanged
is expected again but then the IsHoming
returns false. The result should be that the OnOk
event will be received.
It will also test that the correct homeTimeout value is used.
|
|
This second happy flow scenario covers that the homing is finished immediately. The IsHoming
returns false the first time and the async
interface is used to make the OnOk
reply asynchronously.
|
|
The next test is about receiving a timeout when starting takes too long. It also makes sure that stop is called twice. Once before homing, and once after the timeout.
|
|
The last test makes sure that stop is called on the glue when the starting is aborted.
|
|
Testing the Rest
Some more tests could be defined around the error behavior. The Stop
call can fail, the Home
call can fail and the IsHoming
call can fail at different moments. I think it would be wise to make sure that a glue.Stop()
is attempted after something goes wrong after starting to home. But I have not added these tests in this example.
The Move
tests are like the Start
tests and are not included in this article. These tests will also test that the position is correctly passed to the glue.
Test Results
Below is some output from running the test. The test output is intermingled with Dezyne trace output. Normally this should be redirected elsewhere.
Running main() from gmock_main.cc
[==========] Running 9 tests from 3 test suites.
[----------] Global test environment set-up.
[----------] 1 test from MotorImplTest
[ RUN ] MotorImplTest.MotorIdIsPassedAroundAndInitializeReturnsOk
<external>.api.Initialize -> MotorImpl.api.Initialize
MotorImpl.config.GetHomeTimeout -> ConfigMock.api.GetHomeTimeout
MotorImpl.config.Result:Ok <- ConfigMock.api.Result:Ok
MotorImpl.config.GetMoveTimeout -> ConfigMock.api.GetMoveTimeout
MotorImpl.config.Result:Ok <- ConfigMock.api.Result:Ok
MotorImpl.glue.Initialize -> MotorGlueMock.api.Initialize
MotorImpl.glue.Result:Ok <- MotorGlueMock.api.Result:Ok
<external>.api.Result:Ok <- MotorImpl.api.Result:Ok
[ OK ] MotorImplTest.MotorIdIsPassedAroundAndInitializeReturnsOk (0 ms)
[----------] 1 test from MotorImplTest (0 ms total)
[----------] 4 tests from MotorImplTest_Idle
[ RUN ] MotorImplTest_Idle.StartResultsInOnOk
<external>.api.Initialize -> MotorImpl.api.Initialize
MotorImpl.config.GetHomeTimeout -> ConfigMock.api.GetHomeTimeout
MotorImpl.config.Result:Ok <- ConfigMock.api.Result:Ok
MotorImpl.config.GetMoveTimeout -> ConfigMock.api.GetMoveTimeout
MotorImpl.config.Result:Ok <- ConfigMock.api.Result:Ok
MotorImpl.glue.Initialize -> MotorGlueMock.api.Initialize
MotorImpl.glue.Result:Ok <- MotorGlueMock.api.Result:Ok
<external>.api.Result:Ok <- MotorImpl.api.Result:Ok
<external>.api.Start -> MotorImpl.api.Start
MotorImpl.glue.Stop -> MotorGlueMock.api.Stop
MotorImpl.glue.Result:Ok <- MotorGlueMock.api.Result:Ok
MotorImpl.glue.Home -> MotorGlueMock.api.Home
MotorImpl.glue.Result:Ok <- MotorGlueMock.api.Result:Ok
MotorImpl.glue.IsHoming -> MotorGlueMock.api.IsHoming
MotorImpl.glue.Result:True <- MotorGlueMock.api.Result:True
MotorImpl.timer.Create -> TimerMock.api.Create
MotorImpl.timer.return <- TimerMock.api.return
<external>.api.return <- MotorImpl.api.return
MotorImpl.<q> <- MotorGlueMock.api.OnHomingChanged
MotorImpl.glue.OnHomingChanged <- MotorImpl.<q>
MotorImpl.glue.IsHoming -> MotorGlueMock.api.IsHoming
MotorImpl.glue.Result:True <- MotorGlueMock.api.Result:True
MotorImpl.<q> <- MotorGlueMock.api.OnHomingChanged
MotorImpl.glue.OnHomingChanged <- MotorImpl.<q>
MotorImpl.glue.IsHoming -> MotorGlueMock.api.IsHoming
MotorImpl.glue.Result:False <- MotorGlueMock.api.Result:False
MotorImpl.timer.Cancel -> TimerMock.api.Cancel
MotorImpl.timer.return <- TimerMock.api.return
<external>.api.OnOk <- MotorImpl.api.OnOk
[ OK ] MotorImplTest_Idle.StartResultsInOnOk (0 ms)
[ RUN ] MotorImplTest_Idle.StartResultInOnOkWhenItFinishedDirectly
<external>.api.Initialize -> MotorImpl.api.Initialize
MotorImpl.config.GetHomeTimeout -> ConfigMock.api.GetHomeTimeout
MotorImpl.config.Result:Ok <- ConfigMock.api.Result:Ok
MotorImpl.config.GetMoveTimeout -> ConfigMock.api.GetMoveTimeout
MotorImpl.config.Result:Ok <- ConfigMock.api.Result:Ok
MotorImpl.glue.Initialize -> MotorGlueMock.api.Initialize
MotorImpl.glue.Result:Ok <- MotorGlueMock.api.Result:Ok
<external>.api.Result:Ok <- MotorImpl.api.Result:Ok
<external>.api.Start -> MotorImpl.api.Start
MotorImpl.glue.Stop -> MotorGlueMock.api.Stop
MotorImpl.glue.Result:Ok <- MotorGlueMock.api.Result:Ok
MotorImpl.glue.Home -> MotorGlueMock.api.Home
MotorImpl.glue.Result:Ok <- MotorGlueMock.api.Result:Ok
MotorImpl.glue.IsHoming -> MotorGlueMock.api.IsHoming
MotorImpl.glue.Result:False <- MotorGlueMock.api.Result:False
MotorImpl.async.req -> AsyncMock.api.req
MotorImpl.async.return <- AsyncMock.api.return
<external>.api.return <- MotorImpl.api.return
MotorImpl.<q> <- AsyncMock.api.ack
MotorImpl.async.ack <- MotorImpl.<q>
<external>.api.OnOk <- MotorImpl.api.OnOk
[ OK ] MotorImplTest_Idle.StartResultInOnOkWhenItFinishedDirectly (0 ms)
[ RUN ] MotorImplTest_Idle.StartResultsInOnErrorAfterTimeout
<external>.api.Initialize -> MotorImpl.api.Initialize
MotorImpl.config.GetHomeTimeout -> ConfigMock.api.GetHomeTimeout
MotorImpl.config.Result:Ok <- ConfigMock.api.Result:Ok
MotorImpl.config.GetMoveTimeout -> ConfigMock.api.GetMoveTimeout
MotorImpl.config.Result:Ok <- ConfigMock.api.Result:Ok
MotorImpl.glue.Initialize -> MotorGlueMock.api.Initialize
MotorImpl.glue.Result:Ok <- MotorGlueMock.api.Result:Ok
<external>.api.Result:Ok <- MotorImpl.api.Result:Ok
<external>.api.Start -> MotorImpl.api.Start
MotorImpl.glue.Stop -> MotorGlueMock.api.Stop
MotorImpl.glue.Result:Ok <- MotorGlueMock.api.Result:Ok
MotorImpl.glue.Home -> MotorGlueMock.api.Home
MotorImpl.glue.Result:Ok <- MotorGlueMock.api.Result:Ok
MotorImpl.glue.IsHoming -> MotorGlueMock.api.IsHoming
MotorImpl.glue.Result:True <- MotorGlueMock.api.Result:True
MotorImpl.timer.Create -> TimerMock.api.Create
MotorImpl.timer.return <- TimerMock.api.return
<external>.api.return <- MotorImpl.api.return
MotorImpl.<q> <- MotorGlueMock.api.OnHomingChanged
MotorImpl.glue.OnHomingChanged <- MotorImpl.<q>
MotorImpl.glue.IsHoming -> MotorGlueMock.api.IsHoming
MotorImpl.glue.Result:True <- MotorGlueMock.api.Result:True
MotorImpl.<q> <- TimerMock.api.OnTimeout
MotorImpl.timer.OnTimeout <- MotorImpl.<q>
MotorImpl.glue.Stop -> MotorGlueMock.api.Stop
MotorImpl.glue.Result:Ok <- MotorGlueMock.api.Result:Ok
<external>.api.OnError <- MotorImpl.api.OnError
[ OK ] MotorImplTest_Idle.StartResultsInOnErrorAfterTimeout (0 ms)
[ RUN ] MotorImplTest_Idle.WhileStartingAndThenStopThenGlueStopIsCalled
<external>.api.Initialize -> MotorImpl.api.Initialize
MotorImpl.config.GetHomeTimeout -> ConfigMock.api.GetHomeTimeout
MotorImpl.config.Result:Ok <- ConfigMock.api.Result:Ok
MotorImpl.config.GetMoveTimeout -> ConfigMock.api.GetMoveTimeout
MotorImpl.config.Result:Ok <- ConfigMock.api.Result:Ok
MotorImpl.glue.Initialize -> MotorGlueMock.api.Initialize
MotorImpl.glue.Result:Ok <- MotorGlueMock.api.Result:Ok
<external>.api.Result:Ok <- MotorImpl.api.Result:Ok
<external>.api.Start -> MotorImpl.api.Start
MotorImpl.glue.Stop -> MotorGlueMock.api.Stop
MotorImpl.glue.Result:Ok <- MotorGlueMock.api.Result:Ok
MotorImpl.glue.Home -> MotorGlueMock.api.Home
MotorImpl.glue.Result:Ok <- MotorGlueMock.api.Result:Ok
MotorImpl.glue.IsHoming -> MotorGlueMock.api.IsHoming
MotorImpl.glue.Result:True <- MotorGlueMock.api.Result:True
MotorImpl.timer.Create -> TimerMock.api.Create
MotorImpl.timer.return <- TimerMock.api.return
<external>.api.return <- MotorImpl.api.return
MotorImpl.<q> <- MotorGlueMock.api.OnHomingChanged
MotorImpl.glue.OnHomingChanged <- MotorImpl.<q>
MotorImpl.glue.IsHoming -> MotorGlueMock.api.IsHoming
MotorImpl.glue.Result:True <- MotorGlueMock.api.Result:True
<external>.api.Stop -> MotorImpl.api.Stop
MotorImpl.timer.Cancel -> TimerMock.api.Cancel
MotorImpl.timer.return <- TimerMock.api.return
MotorImpl.glue.Stop -> MotorGlueMock.api.Stop
MotorImpl.glue.Result:Ok <- MotorGlueMock.api.Result:Ok
<external>.api.return <- MotorImpl.api.return
[ OK ] MotorImplTest_Idle.WhileStartingAndThenStopThenGlueStopIsCalled (0 ms)
[----------] 4 tests from MotorImplTest_Idle (2 ms total)
... snip ...
[----------] Global test environment tear-down
[==========] 9 tests from 3 test suites ran. (5 ms total)
[ PASSED ] 9 tests.
Download and Run Code
The code can be downloaded here. I have only tested it on Linux. Dezyne 2.8.1 or higher must be installed and in the search path.
Use the following commands to run the unit tests:
unzip code.zip
cd unittesting
mkdir build
cd build
cmake .. -DDEZYNE_RUNTIME_PATH=/opt/dezyne/runtime/c++
make
./DezyneTest
Visual Studio Code settings have been included. Update the settings.json
to point to the Dezyne runtime before using it.
Conclusion
Even when using Dezyne with formal verification some testing needs to be done. Especially the functional behavior and data handling needs to be tested. I have shown how and what to test for the MotorImpl
component. The behavior of this component especially needs to be tested because it uses the permissive MotorGlue
, and the knowledge of how to use the glue is contained in MotorImpl
. The unit tests check usage scenarios of the glue.