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.