Testing finite state machines in C++
Tags: cpp, testing, fsm
Published: 2023-05-29 12:25:41

## Introduction

The state machine design pattern is an incredibly useful tool for writing well structured and testable software. However, it quickly becomes difficult to efficiently test software components that make use of finite state machines (FSM) without repeating boilerplate code. In my experience, most of this boilerplate code is produced by providing stimuli to the component to transition to the desired state for a test case. The project layout and methodology presented in this article aim to improve test readability and maintainability. All source code is available on my [GitHub](https://github.com/twendt97/independent_fsm).

This article is based on the [classic UML state pattern](https://refactoring.guru/design-patterns/state). In order to understand the ideas presented in the following paragraphs it is necessary to have a good understand of this design pattern.

## Project Layout

The project follows a component oriented approach. Consider the directory structure of the repository. The ``include`` directory contains the public interface of the component which only consists of a pure abstract class a.k.a. interface in C++. Furthermore, the directory contains the declaration of the factory which returns an implementation of the public interface.

The ``src`` directory contains implementation private source and header files which are fully consumed into the compiled library. The user of the component does not have access to this code. The component diagram below reflects the macro structure of the project. The user only has access to the public interface while the concrete implementation is opaquely instantiated through a simple factory. The ``states`` component is project internal and not accessible through the public interface.

![Component diagram of the project](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f4vk5biikb891v6f2pcd.png)

## Architecture

The program developed for this example simulates the behavior of a dog which offers the following functions:

* Pet (state independent)
* Perform a command that triggers a state transition (only possible in states ``STANDING``, ``SITTING`` and ``LAYING``)
* Feed (only possible in state ``SITTING`` and state does not change)
* Sleep (only possible in state ``LAYING``)
* Wake up (only possible in state ``SLEEPING``)

The state machine below summarizes the state transitions of the dog:

![Finite State Machine of the test component ``Dog``](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3ojgmgqdjm5fw8b6vxyw.png)

Analyzing the situation from a testing perspective we can define three equivalence classes (EC):

1. State independent functions
2. State dependent functions that do not trigger a state transition
3. State dependent functions that trigger a state transition

In order to test the first EC it is desirable to have control over the state classes which are injected in the state dependent class. Since the functionality is state independent it must also expose the same behavior if *no state is present*. Therefore, state independent functionality may not use any functions which are defined in the state classes. In test cases this is asserted by simply providing an implementation that aborts the test in case a state function is called. Furthermore, the ability to artificially put the FSM in a certain state to test the non transitional behavior is important to test the second EC. The third EC is tested by providing a stimulus through the public interface and to assert correct transitioning. These requirements lead to the following architecture where the interface ``Dog`` is the public interface from the component diagram above:

![Class diagram of the test component ``Dog``](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qsummy4qihjpyyoqfp8s.png)

The introduction of the class ``SharedAttributes`` enables the injection of an alternative implementation of the ``State`` interface as well as the artificial maneuvering of the FSM through the function ``setCurrentState`` for testing the first and second EC. Moreover, this class may be used to share resources like state instances, threads, mutexes etc. between states and the states owner. In production the ``State`` interface implementation is reflected by the following class diagram:

![Class diagram of the ``States`` component](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hv6a1ta9k3dj4id92zlg.png)

The class ``BaseState`` has been introduced because it is not possible to mock abstract classes with member variables with the chosen mocking framework [FakeIt](https://github.com/eranpeer/FakeIt). Nevertheless, the concrete state implementations need access to the ``SharedAttributes`` class which is a protected member of ``BaseState``. Conveniently, ``BaseState`` provides default implementations for certain functions which may be overwritten by the concrete states.

Using the design pattern "abstract factory" to instantiate the ``states`` component enables the provision of a factory which returns an alternative implementation for the states. This has been done in the state independent test cases.

## Tests

The first two ECs are white box tests since they need access to the declarations of ``State`` and ``StateFactory`` which are not accessible through the public interface. This is reflected by the include directories for these targets which give access to the private ``src`` directory. For state independent functions the interface ``State`` is mocked and every function call throws an exception which aborts the test. For testing state functionality a handle to the ``m_attributes`` member of ``DogImpl`` is kept to manually set states. 

The last EC is a black box test: The ``DogImpl`` class is instantiated through the factory provided in the public interface and the corresponding target only has access to the public ``include`` directory. Thereafter, appropriate stimulus is applied to the component and correct state transitioning is asserted. 

This test methodology ensures that each aspect of the FSM is only tested once following the DRY principle. It produces highly readable and maintainable tests.