State machines is like a lot for embedded software, and for sure can be tested in all of their forms, but lest begin with the most simple example. A state machine that with three states that only moves the next state on each one.

This is a good example of why is important not only write code but write a code that can be tested, and this is why our state machine accepts a parameter and return another one.

app/dummy.c

#include <stdint.h>
#include "dummy.h"

/*simple state machine with three states that does nothing,
the function accapt the current state to run*/
uint8_t state_machine( uint8_t state )
{
    switch( state )
    {
        case STATE_1:
            state = STATE_2;
        break;

        case STATE_2:
            state = STATE_1;
        break;

        default:
            state = STATE_1;
        break;
    }
    /*return next state to transition*/
    return state;
}

Lets write state definitions for each state in the header file, this will make our test file can access to the same definitions. Another example of writing code that can be tested

app/dummy.h

#ifndef __DUMMY_H__
#define __DUMMY_H__

/*state definitions*/
#define STATE_1 0
#define STATE_2 1

uint8_t state_machine( uint8_t state );

#endif // __DUMMY_H__

We write three different test cases each of them for the three potential transitions our simple state machine can have. nothing out of the ordinary.

test/test_dummy.c

void test__state_machine__from_state1_to_state2(void)
{
    /*test transition from state 1 to state 2*/
    uint8_t n_state = state_machine( STATE_1 );
    TEST_ASSERT_EQUAL_MESSAGE( STATE_2, n_state, "state machine do not transitioned to state 2" );
}

void test__state_machine__from_state2_to_state1(void)
{
    /*test transition from state 2 to state 1*/
    uint8_t n_state = state_machine( STATE_2 );
    TEST_ASSERT_EQUAL_MESSAGE( STATE_1, n_state, "state machine do not transitioned to state 1" );
}

void test__state_machine__from_default_to_state1(void)
{
    /*test transition from default to state 1, we passa number 
    that do not represent any of the states*/
    uint8_t n_state = state_machine( 3u );
    TEST_ASSERT_EQUAL_MESSAGE( STATE_1, n_state, "state machine do not transitioned to state 1" );
}

More complex state machines

100% of the time state machines are more complex that the last example with internal operations and conditions on each state, the trick to test more complex state machines is to avoid as much as possible local variables and use structures with elements like the current or next state to transition plus all the variables needed internally by the state machine.

In the following example we pass a pointer to control structure containing an element to move the state machine plus just a variable to condition a transition, as before we return the next state to transition, to evaluate the state machine has move, (even though this can be replace by the same Machine->state element)

app/dummy.c

#include <stdint.h>
#include "dummy.h"
 
/*simple state machine with three states that does nothing,
the function accapt the control structure*/
uint8_t state_machine( Machine *machine )
{
    switch( machine->state )
    {
        case STATE_1:
            /*condition the transition on var value*/
            if( machine->var == 1u ){
                machine->state = STATE_2;
            }
        break;

        case STATE_2:
            /*reset var value*/
            machine->var = 0u;
            machine->state = STATE_1;
        break;

        default:
            machine->state = STATE_1;
        break;
    }
    /*return next state to transition*/
    return machine->state;
}

the structure definition is placed in header file because it is necessary to be access by the unit test file, where we need to declare structure of the same type.

app/dummy.h

#ifndef __DUMMY_H__
#define __DUMMY_H__

/*state definitions*/
#define STATE_1 0
#define STATE_2 1

/*state machine control structure*/
typedef struct
{
    uint8_t state; /*state machine control*/
    uint8_t var;   /*variabel internaly by the machine */
} Machine;

uint8_t state_machine( Machine *state );

#endif // __DUMMY_H__

We can see in the unit tests how we can init with values the control structures and pre-set the element var which in turn condition the transition from one of the state to another. See also that in some of the state we can test the var value to corroborate the state is doing what is supposed to do.

test/test_dummy.c

void test__state_machine__from_state1_to_state1(void)
{
    Machine mch = {.state = STATE_1, .var = 0u };
    /*test transition from state 1 to state 1*/
    uint8_t n_state = state_machine( &mch );
    TEST_ASSERT_EQUAL_MESSAGE( STATE_1, n_state, "state machine do not stay in the same state 1" );
}

void test__state_machine__from_state1_to_state2(void)
{
    Machine mch = {.state = STATE_1, .var = 1u };
    /*test transition from state 1 to state 2*/
    uint8_t n_state = state_machine( &mch );
    TEST_ASSERT_EQUAL_MESSAGE( STATE_2, n_state, "state machine do not transitioned to state 2" );
}

void test__state_machine__from_state2_to_state1(void)
{
    Machine mch = {.state = STATE_2, .var = 1u };
    /*test transition from state 2 to state 1*/
    uint8_t n_state = state_machine( &mch );
    TEST_ASSERT_EQUAL_MESSAGE( 0u, mch.var, "var element is not cleared" );
    TEST_ASSERT_EQUAL_MESSAGE( STATE_1, n_state, "state machine do not transitioned to state 1" );
}

void test__state_machine__from_default_to_state1(void)
{
    Machine mch = {.state = 3u, .var = 1u };
    /*test transition from default to state 1, we passa number 
    that do not represent any of the states*/
    uint8_t n_state = state_machine( &mch );
    TEST_ASSERT_EQUAL_MESSAGE( 1u, mch.var, "var element do not remian unaltered" );
    TEST_ASSERT_EQUAL_MESSAGE( STATE_1, n_state, "state machine do not transitioned to state 1" );
}