Avoiding mocks by enqueuing events

Note: This is a guest post from Allan McInnes, an old colleague of mine. He wrote it to expand upon some of the ideas I introduced in my post about event-based interfaces. Most of my thoughts about event-based systems were shaped by working with Allan, so really I owe the basis for those ideas to him. Actually, he’s shaped a lot of my thoughts on software development — particularly around requirements and architecture — where he’s kind of a guru. He reads more academic papers than any other software developer I know and he’s a constant source of interesting (and mostly good!) ideas. I hope you enjoy it.

You can find more about Allan and some of his work at his blog: allanmcinnes.github.io/.

Including mocks in your tests means that those tests know a lot about the internal implementation of the unit under test. Make a change in the interface of any mocked module, and you not only drive changes in every caller of that interface, you create a cascading series of changes in the tests for each of those callers as well.

Because tests that rely on mocks are prone to breakage, or brittle, they're an active disincentive for making changes to existing code. You're forced to either update the tests with every change you make, allow the tests to break and lose the benefits of unit testing, or just avoid making changes to the code.

In an earlier post, Matt showed how to use events instead of function calls to simplify the functional interface of module. A simpler functional interface means a simpler mock, and less brittle tests.

But it turns out that you can go a step further.

A step further out

By inserting an event queue between the modules that produce events and the modules that consume events you can decouple the producers from the consumers. That's great from an architectural perspective. But it also has some benefits when it comes to testing. Let's look at an example to see what those benefits are (you can find full source code for this example at https://github.com/allanmcinnes/SimpleEventExample).

In this example, the event queue is a self-contained module that can be unit-tested by itself, and has the following interface:

bool event_queue_isEmpty(void);           // True if nothing in the queue
bool event_queue_append(event_t* event);  // Enqueue by copying into the queue
event_t* event_queue_getNextEvent(void);  // Reference to the head of the queue
void event_queue_consumeNextEvent(void);  // Remove the current head event

I'm not going to get into all of the tradeoffs and design decisions involved in implementing an event queue here. If you're interested in that kind of thing, it's worth reading through Robert Nystrom's writeup on the event queue pattern. Nystrom provides a great rundown of the pros, cons, and design considerations of an event-queue, most of which are as applicable to embedded systems as they are to games.

Event producers use event_queue_append() to enqueue new events. The append operation is the producer's sole interface to the rest of the system. For example, here's a producer enqueuing an event signalling a change in the setting of a volume knob:

static inline void new_knob_event(knob_t knob, uint8_t level) {
  event_t event;
  int percent = (100 * level) / 255;
  event.type = EVENT_KNOB_SET;
  event.knob_set.knob = knob;
  event.knob_set.percent_of_max = percent;
  event_queue_append(&event);
}

Mock-free tests

What I've shown you so far may not look too different to what Matt showed in his initial post on this topic. But if you look back at that previous post you'll see that Matt ended up having to mock the module that receives the events so that the behavior of the producer could be isolated for testing. With the event queue in the mix, the event consumer is already isolated from the producer.

Instead of using a mock, you can just combine the producer and the event queue into a single "unit under test". There's no need to "expect" or "ignore" any function calls. Just execute the test actions, and then verify the outcome by making assertions about contents of the event queue:

void test_event_producer_WhenMultipleGPIOInterrupts_EnqueuesMultipleEventsInInterruptOrder(void)
{
    // Test actions
    event_producer_gpio_interrupt_handler(GPIO_PLAY | GPIO_LEVEL_HIGH);
    event_producer_gpio_interrupt_handler(GPIO_SWITCH_AUX );

    // Verify the state of the queue matches expectations
    event_t* event;
    TEST_ASSERT(!event_queue_isEmpty());
    event = event_queue_getNextEvent();
    TEST_ASSERT(event->type == EVENT_BUTTON_PRESSED);
    TEST_ASSERT(event->button == BUTTON_PLAY);
    event_queue_consumeNextEvent();

    event = event_queue_getNextEvent();
    TEST_ASSERT(event->type == EVENT_SWITCH_SET);
    TEST_ASSERT(event->switch_set.switch_id == SWITCH_AUX);
    TEST_ASSERT(event->switch_set.on == false);
}

Event loops for event queues

What about event consumers? How do consumers get events if they're not getting them directly from the event producers? One of the most common approaches in small embedded systems is to use a simple event loop (the same pattern is common in GUIs and games).

In the event-loop approach, interrupts wake the processor from a low-power sleep and cause one or more events to be enqueued. When control returns to the main loop an executive iterates through the event queue, dispatching events to all consumers (essentially a simple, centralized Publish-Subscribe pattern):

void event_executive_run(void) {
  while (!event_queue_isEmpty()) {  
    for (int i = 0; i < registered_consumer_count; i++) {
      registered_consumers[i](event_queue_getNextEvent());
    }
    event_queue_consumeNextEvent();
  }
}

Each consumer responds to the events in a run-to-complete manner, possibly enqueuing new events. When there are no events left in the queue, the processor can be put back to sleep. This is a useful pattern for low-power embedded systems. Some widely-deployed real-world examples of this approach in embedded systems include the Quantum Platform and TinyOS.

Why bother?

Ok, so we've added an event queue and an executive, and ended up with a system that's more complex than what Matt originally showed. Is it worth it?

Well, mocks are a sign of coupling in a system. The event queue breaks that coupling (which we often want to do for architectural reasons anyway). As we saw above, by adding an event queue the tests for event producers can be built without mocks. In fact, none of the unit tests in the example code require any mocks to execute (well, ok, the tests for the executive include some local fake functions that are something like mocks, but that's it). Mock-free unit tests are simpler and less brittle.

Up to now, I've been focusing on the advantages of event queues from a testing perspective. But there are other reasons to use an event queue:

  • From an architectural perspective, your system can be structured as a collection of state-machine-like units that consume events and produce new events in response. That gives you units with small interfaces and a regular structure, so they're easier to design, easier to review, and more likely to be right.
  • In a system that uses interrupts, the event queue provides a convenient and uniform way to separate immediate interrupt handling from any later main-loop actions that might be triggered by an interrupt. Instead of using arbitrary flag variables to signal a need for main-loop actions, you can just enqueue events from your interrupt handlers.

Sometimes your system is so small and simple that a queue doesn't make sense. And sometimes it's so complex that you're working with an RTOS that has built-in facilities for messaging and multi-tasking. But for a lot of medium-complexity bare-metal embedded systems a queue-based event-driven architecture can be a good choice.