Event-based interfaces for testability

Unit testing is great for verifying the behavior of individual modules, but how do you put those modules together in a way that makes things testable?

One of the most useful ways I've found to do this is to think about the system in terms of events.

One of the biggest challenges you'll face when trying to unit test individual modules is if your modules are tightly coupled.

Modules interact with each other through function calls. But function call interfaces have a way of getting out of hand.

You start with a single call and then you add more and more calls between modules. At a certain point, with all these function calls, your modules are tightly coupled. The behavior of each of the modules is highly dependent on the other, and the interface between the modules make them hard to unit test individually.

Using events in the interfaces between your modules can help you decouple them -- making them simpler and easier to unit test and easier to unit test.

What are events?

What are events? Well, they're just data structures that you can pass around your system, that indicate something has happened or some state has changed in for system.

In an embedded system an event might be something generated externally -- like a button press or a character received at a serial port. Events like these might be an input to a button or serial processor module.

Events could also be generated by modules as outputs as well -- like if the state of the system changes in response to a button press, you might generate an event to indicate the state change.

The event is the interface

Once we have events, we can define the interfaces between our modules in terms of passing events instead of just making function calls.

Consider an interface to a module that manages some sort of global state to the system. It might care about all of our user inputs, and have functions like this:

void state_machine_button_pressed(button_t button);
void state_machine_button_released(button_t button);
void state_machine_knob_set(knob_t knob, int percent_of_max);
void state_machine_switch_set(switch_t switch, bool on);

In this case these function calls are the interface to this state machine module. However, function calls like this between modules are what can make them difficult to unit test.

If we wanted to unit test the input module responsible for calling these functions we could mock all of these function calls in our tests and verify that they are called correctly.

But these sorts of tests are often brittle and will require more maintenance over time. This is because if any of the functions of the interface are changed, the tests will need to be updated as well (or more likely, the tests will break but nobody will invest the time to fix them and the tests will rot away).

But consider a different interface:

void state_machine_send_event(event_t * event);

In this case we have a single function interface and the real interface to this module is the data passed in through the event_t argument (the event_t type is a custom data structure that allows us to pass all of the same information as before -- we'll look at this more closely in a bit).

Now if we want to test a module that uses this interface, we only need to mock this single state_machine_send_event function, and verify that the correct events are passed. This single function is much less likely to change over time so our tests won't keep breaking whenever we add a new function (or change an old one).

Implementation

But how do we make this magic state_machine_send_event function work? Here's a simple event_t structure:

typedef enum {
  EVENT_BUTTON_PRESSED,
  EVENT_BUTTON_RELEASED,
  EVENT_KNOB_SET,
  EVENT_SWITCH_SET,
} event_type_t;

typedef struct {
  event_type_t type;
} event_t;

In this case event_t is just a data structure that consists of a type that tells is what type of event it is.

This is simple enough, but we really need to be able to have different types of data provided along with different types of events. In an object oriented language we could easily use inheritance for this. In C however, we can use kind of a trick to simulate inheritance. We can define individual event structures for each event type, but each new event structure must include the original event_t as its first element.

For example:

typedef struct {
  event_t event;
  button_t button;
} event_button_pressed_t;

typedef struct {
  event_t event;
  button_t button;
} event_button_released_t;

typedef struct {
  event_t event;
  knob_t knob;
  int percent_of_max;
} event_knob_set_t;

typedef struct {
  event_t event;
  switch_t switch;
  bool on;
} event_switch_set_t;

The key to this setup is that a pointer to any of the custom event types (e.g. event_button_pressed_t, event_button_released_t, etc.) is a pointer to an event_t, and we can safely cast between the two.

So, we can call our state_machine_send_event function by creating a new event like this:

event_button_pressed_t button_event = {
  .event = EVENT_BUTTON_PRESSED,
  .button = ENTER_BUTTON,
}

state_machine_send_event((event_t *)&button_event);

And then then inside of our state_machine_send_event function we can switch on the event_t type and cast to correct type:

void state_machine_send_event (event_t * event)
{
  switch (event->type)
  {
    case EVENT_BUTTON_PRESSED:
    {
      event_button_pressed_t * e = (event_button_pressed_t *)event;
      // Access data from this event with e->button.
      break;
    }
    case EVENT_BUTTON_RELEASED:
    {
      event_button_released_t * e = (event_button_released_t *)event;
      // Access data from this event with e->button.
      break;
    }
    case EVENT_KNOB_SET:
    {
      event_knob_set_t * e = (event_knob_set_t *)event;
      // Access data from this event with e->knob and e->percent_of_max.
      break;
    }
    case EVENT_SWITCH_SET:
    {
      event_switch_set_t * e = (event_switch_set_t *)event;
      // Access data from this event with e->switch and e->on.
      break;
    }
  }
}

Applications

This approach can could be used in a variety of ways.

For simpler, bare-metal systems, I've had success with a single event queue. All events generated in the system go into a single queue and then these events are handled in the order in which they occurred.

If you're running on an RTOS, you can more easily use multiple queues for passing events between multiple tasks.