When you want to unit test... abstract the hardware

So you write embedded software in C and you think that unit testing might help you do it better. You already know about creating well-defined software modules and how this makes it easier to write unit tests. But what else can you do to make these modules easier to test? What are some more coding patterns that make unit testing easier?

Don't directly interface the hardware

One of the biggest problems with trying to unit test embedded software is the hardware.

The hardware is what makes embedded software interesting. It's what puts our software out in the real world and gives us the ability to do real things with real people. It's what takes our software beyond the screen of a computer or a phone.

But it also makes unit testing harder.

Typically your code has to read and write hardware registers to control your processor and make it do these real things -- like communicating with sensors over I2C, detecting button presses, or controlling LEDs. When your code needs to interact with the hardware like this, it's harder to unit test.

So how do you make it easier to test?

You do not want to access the hardware directly from the modules that you want to unit test. If you do, it will make your code a lot harder to test.

Create a hardware abstraction layer

Instead, whenever you need to interface the hardware you want to create another module that "wraps" or "abstracts" the hardware access. Sometimes this is called a hardware abstraction layer (HAL).

For example if you're turning an LED, don't directly write to the hardware register controlling the LED pin. You should create another LED module that has functions for turning on and off the LEDs.

This approach makes your unit tests much easier to write because you can use FFF or CMock to automatically generate mocks for the functions in your HAL. Then during your tests you can substitute these mocks for the real HAL and verify that the correct HAL functions are called. For example, we might know the LED was turned on when led_turn_on() is called.

You can also simulate inputs like button presses by mocking the return values of functions. If you have some button processing logic, you might simulate all the possible button interactions by changing what is_button_pressed() returns during your tests.

A more specific example

Here is some button debounce code (note that this is an oversimplied example -- don't use this as real debouce code). Button debouncing logic is a good candidate for testing. Here's how it might look without a HAL:

#include <some_hw_memory_map.h>

static int how_long_button_has_been_pressed = 0;

// Call this periodically to debounce button inputs.
// Returns true if the button is pressed, false if not pressed.
bool debounce_is_button_pressed() {

  if (PORTE & 0x04) {
    how_long_button_has_been_pressed++;
  } else {
    how_long_button_has_been_pressed = 0;
  }

  return (how_long_button_has_been_pressed > 10);
}

If we want to test this, what do we do about PORTE? Yes, we could create our own fake version of some_hw_memory_map.h where we create our own version of PORTE which we manipulate during our tests.

However if some_hw_memory_map.h is like most files provided by hardware vendors, this file probably includes bunch of other headers that will make setting up a fake more difficult.

The simpler, and cleaner aproach is to insert a HAL function call here (hal_is_button_pressed()) to check if the button is pressed. Then we don't include #include <some_hw_memory_map.h>, we just include our HAL module:

#include "hal_button.h"

static int how_long_button_has_been_pressed = 0;

// Call this periodically to debounce button inputs.
// Returns true if the button is pressed, false if not pressed.
bool debounce_is_button_pressed() {

  if (hal_is_button_pressed()) {
    how_long_button_has_been_pressed++;
  } else {
    how_long_button_has_been_pressed = 0;
  }

  return (how_long_button_has_been_pressed > 10);
}

The HAL function in hal_button.c might look like:

#include <some_hw_memory_map.h>

bool hal_is_button_pressed() {
    return (PORTE & 0x04);
}

We've moved the hardware dependency out of the button debounce logic and into the HAL. Now we can test our button debounce module without any dependencies on the hardware, just by mocking hal_is_button_pressed(). And mocking functions is easy (and and automatic!) with Ceedling and CMock or FFF.

There are other benefits too

The unit tests created by mocking HAL functions will be much more expressive and easier to understand. Rather than checking if bit 7 of PORTE is set (wait does "set" mean the LED is on or off??), when you assert that led_turn_on() is called it's clear that you expect the LED to be turned on.

Also since you're not using any of the target hardware, it's much easier to run your tests on your host PC. And you want to run your tests on your host PC because it's waaay faster.

Try this at home

Do you have any code that accesses PORTE, STK_CVR or UCA0TXBUF directly from your application logic? The next time you need to access the hardware from some code to you want to unit test, think about how you could create a hardware abstraction instead. This will make your code clearer and easier to unit test.