How to Unit Test Finite State Machines

We use a lot of state machines in our projects. We use them for abstracting instruments that we control, controlling when user input controls have to be enabled or disabled and for other things.

State machines are great for these kind of tasks (much easier that nested switch statements anyway) but they provide a big challenge when developing software test driven. This is due to the fact that they are of course very state full and often active (running on their own worker thread).

Here are some best practices leading to maintainable and refactoring friendly unit tests.

Decouple internal and external states

We use state machine of course for representing the states an object, system, instrument or some other thing can be in. However, there are two different “layers” of states:

  • internal states – the states that the state machine uses to do its job
  • external states – states that can be reacted to by code using your state machine (events or side-effects caused by transition actions)

For unit tests to be refactoring friendly it is important that they are written to arrange and assert external states. Doing so you can refactor the internals of the state machine without having to change your tests.

One consequence of this principle is that you should never make assertions against the state the state machine currently is in.

I know that this sounds very counter-intuitive – however consider the following example:

You’ve written a state machine for a very simple elevator with the states: moving up, moving down and stopped. And you have written tests that check the correct behavior. Now you want to distinguish whether there is a passenger on board or not. Therefore, you replace the moving states with four new states: moving up with passenger, moving up without passenger, moving down with passenger, moving down without passenger. If you wrote your tests against internal states then you have to adapt most of them. If you wrote them against external states – for example an event Moving with an event argument holding information whether the elevator is moving up or down – the tests would not be affected, you could simply add an indicator to the event argument whether there is a passenger on board.

Check complete scenarios not single states

When writing your tests, write them from the perspective of the code using the state machine and not with the structure of the internal states in mind.

Arrange the state machine to be in the (external) state before the action you want to test by firing events on the state machine. Do not use any hack to change the internal state directly with some kind of magic – yes, reflection is magic in this case, too.

Then perform the transition by firing the event on the state machine that triggers it.

Finally, assert whether the state machine made the correct calls to its environment. Sometimes, more than a single transition is needed to check for a feature because the effect get only visible by a chain of actions of subsequent transitions.

These kind of tests remain stable even if you refactor the internal states of your state machine or you join or split actions on transitions.

Separate asynchronous aspect from real functionality

Many of the state machines we use are active. They run on their own worker thread so that they don’t block callers. However, multi-threading is not unit testable – you cannot simulate every possible scenario. Therefore, you should be able to test the functionality of your state machine separately from all multi-threading aspects. All your unit and acceptance tests should run synchronously. You should replace your active state machine with a passive one in your tests. I suggest you use an existing state machine component supporting this scenario (see next section).

Use a state machine component

Before you start writing your own generic state machine component, you better take a look at what is already available and hopefully tested. For .Net projects take a look at bbv.Common State Machine.

This way, you can focus on testing your functionality instead of spending time to test basic state machine related stuff – the bbv.Common state machine for example has a total of 172 unit tests and acceptance tests.

It is important that the component you choose is designed to support proper unit testing.

The state machine in bbv.Common is design that

  • it will not swallow exceptions
  • you can easily replace an active state machine with a passive one to get rid of multi-threading difficulties in unit tests (no need for signals) leading to much simpler unit tests and acceptance tests
  • extension support to track additional data while executing tests

Sample state machine

using FluentAssertions;
using Xunit;

public enum States
{
    MovingUp,
    MovingDown,
    Stopped
}

public enum Events
{
    GoUp,
    GoDown,
    Stop
}

// this class defines the state machine
public class Elevator
{
    private readonly IStateMachine<States, Events> machine;

    private readonly IAnnouncer announcer;

    public Elevator(IStateMachine<States, Events> machine, IAnnouncer announcer)
    {
        // the state machine instance is injected so that we can use an active
        // state machine (with its own worker thread) in production and a
        // passive state machine in unit and acceptance tests
        this.machine = machine;
        this.announcer = announcer;

        this.InitializeStateMachine();
    }

    public void Activate()
    {
        this.machine.Initialize(States.Stopped);
        this.machine.Start();
    }

    public void MoveUp()
    {
        this.machine.Fire(Events.GoUp);
    }

    public void MoveDown()
    {
        this.machine.Fire(Events.GoDown);
    }

    public void Stop()
    {
        this.machine.Fire(Events.Stop);
    }

    private void InitializeStateMachine()
    {
        this.machine.In(States.MovingUp)
            .On(Events.Stop).Goto(States.Stopped).Execute(this.AnnounceStoppedMovingUp);

        this.machine.In(States.MovingDown)
            .On(Events.Stop).Goto(States.Stopped).Execute(this.AnnounceStoppedMovingDown);

        this.machine.In(States.Stopped)
            .On(Events.GoUp).Goto(States.MovingUp)
            .On(Events.GoDown).Goto(States.MovingDown);
    }

    private void AnnounceStoppedMovingUp()
    {
        this.announcer.Announce(Resources.StoppedMovingUp);
    }

    private void AnnounceStoppedMovingDown()
    {
        this.announcer.Announce(Resources.StoppedMovingDown);
    }
}

public class Resources
{
    public const string StoppedMovingUp = "you reached a higher level!";
    public const string StoppedMovingDown = "do you want to go up again?";
}

// a dependency of the state machine
public interface IAnnouncer
{
    void Announce(string message);
}

public class ElevatorTest
    : IAnnouncer // implement interface of the dependency of the elevator (no additional mock needed)
{
    private readonly Elevator testee;

    private string announcedMessages = string.Empty;

    public ElevatorTest()
    {
        // inject a passive state machine so that the tests are single-threaded and
        // no synchronization/signals are needed
        this.testee = new Elevator(new PassiveStateMachine<States, Events>(), this);
    }

    [Fact]
    public void MoveUp()
    {
        this.testee.Activate();

        this.testee.MoveUp();
        this.testee.Stop();

        // check that the state machine has made to correct call on the dependency
        // do not check on internal state
        this.announcedMessages.Should().Be(Resources.StoppedMovingUp);
    }

    [Fact]
    public void MoveDown()
    {
        this.testee.Activate();

        this.testee.MoveDown();
        this.testee.Stop();

        this.announcedMessages.Should().Be(Resources.StoppedMovingDown);
    }

    public void Announce(string message)
    {
        this.announcedMessages += message;
    }
}
namespace bbv.Common.StateMachine.Elevator
{
using FluentAssertions;
using Xunit; 

public enum States
{
MovingUp,
MovingDown,
Stopped
}

public enum Events
{
GoUp,
GoDown,
Stop
}

// this class defines the state machine
public class Elevator
{
private readonly IStateMachine<States, Events> machine;

private readonly IAnnouncer announcer;

public Elevator(IStateMachine<States, Events> machine, IAnnouncer announcer)
{
// the state machine instance is injected so that we can use an active
// state machine (with its own worker thread) in production and a
// passive state machine in unit and acceptance tests
this.machine = machine;
this.announcer = announcer;

this.InitializeStateMachine();
}

public void Activate()
{
this.machine.Initialize(States.Stopped);
this.machine.Start();
}

public void MoveUp()
{
this.machine.Fire(Events.GoUp);
}

public void MoveDown()
{
this.machine.Fire(Events.GoDown);
}

public void Stop()
{
this.machine.Fire(Events.Stop);
}

private void InitializeStateMachine()
{
this.machine.In(States.MovingUp)
.On(Events.Stop).Goto(States.Stopped).Execute(this.AnnounceStoppedMovingUp);

this.machine.In(States.MovingDown)
.On(Events.Stop).Goto(States.Stopped).Execute(this.AnnounceStoppedMovingDown);

this.machine.In(States.Stopped)
.On(Events.GoUp).Goto(States.MovingUp)
.On(Events.GoDown).Goto(States.MovingDown);
}

private void AnnounceStoppedMovingUp()
{
this.announcer.Announce(Resources.StoppedMovingUp);
}

private void AnnounceStoppedMovingDown()
{
this.announcer.Announce(Resources.StoppedMovingDown);
}
}

public class Resources
{
public const string StoppedMovingUp = “you reached a higher level!”;
public const string StoppedMovingDown = “do you want to go up again?”;
}

// a dependency of the state machine
public interface IAnnouncer
{
void Announce(string message);
}

public class ElevatorTest
: IAnnouncer // implement interface of the dependency of the elevator (no additional mock needed)
{
private readonly Elevator testee;

private string announcedMessages = string.Empty;

public ElevatorTest()
{
// inject a passive state machine so that the tests are single-threaded and
// no synchronization/signals are needed
this.testee = new Elevator(new PassiveStateMachine<States, Events>(), this);
}

[Fact] public void MoveUp()
{
this.testee.Activate();

this.testee.MoveUp();
this.testee.Stop();

// check that the state machine has made to correct call on the dependency
// do not check on internal state
this.announcedMessages.Should().Be(Resources.StoppedMovingUp);
}

[Fact] public void MoveDown()
{
this.testee.Activate();

this.testee.MoveDown();
this.testee.Stop();

this.announcedMessages.Should().Be(Resources.StoppedMovingDown);
}

public void Announce(string message)
{
this.announcedMessages += message;
}
}
}

About the author

Urs Enzler

6 comments

  • I am not sure, but it seems to me that with your differentiation between testing “external” and “internal” state you are more or less describing the difference between “interaction-based” and “state-based” testing (there are also a lot of discussions about this topic under “London school” vs. “classic school” of TDD).
    Or am I missing something?

    However in your case you “assume” that interactions with collaborators always result in state-changes in the collaborators. You then assert against that state-change.
    I think you could also assert against the expected interaction itself with mocks (i.e. in the elevator example: assert that “Announce” is called with a certain argument).
    What would be the downside of that? Do you think its easier to track the state externally than to formulate expectations against interactions?

  • No, this is not the same. You can use interaction vs. state based testing on the dependencies of the state machine: was the correct method invoked (like you suggest in the second paragraph) or was the state of a dependency correctly changed (the later is preferrable if possible due to maintainability/refactorability).
    However, you should never check whether the state machine itself is in the correct state (the problem here is to differenciate between state of the state machine and state of the class). This would make refactorings very difficult due to changes in actually unrelated unit tests.

  • Ok, thanks.
    Why do you think that tracking change of external state and asserting it is better for maintainability/refactorability than asserting against the interaction itself?

  • @Jonas
    Mainly from experience.

    When the object under test calls a method on a dependency then it will change the state of the dependency (or the dependecy will cause a state change in production code).
    Therefore, my main interest is in the state change and not whether a method is called or not.
    Depending too much on interaction testing can lead to tests checking more than needed or to tests that are too strongly coupled to implementation details.

    And I’ve seen a lot of code where the developer made some assumptions about what a method call on a dependency will cause, but was mistaken and thus introducing a hard to spot defect. After refactoring these tests to state-based tests, the defect was obviously.

    Therefore, state-based is easier to comprehend that interaction-based testing.

    However, interaction based testing is often the only possibility if there is no return value or direct state to check for.

  • Great article. I’ve run into similar issues with earlier projects. However, I can wholeheartingly recommend the Test Data Builder pattern for creating…well…test data in unit test. We even have individual builders for each state, so that we don’t have to bring in the object in the correct state from inside the test itself.

    By the way, it seems you are using FluentAssertions, which is my open-source project. But you didn’t list in your previous post about the tools you use.

  • @Dennis Doomen
    Thanks.
    We use the test data builder pattern quiet a lot, too. Not only for state machines, but for all scenarios where a lot of state or data has to be built up before executing the action to verify.

    And I added FluentAssertions to the list of tools in my previous post. I hope I got’em all now 😉

By Urs Enzler

Recent Posts