Modern Unit Testing and Embedded C (Is it possible?)


Are you programming in an Embedded C environment and wonder if Unit Testing is even possible? There is a lot of skepticism about it, so let us shine some light on this topic.

Unit Testing in Embedded C is very possible. You can use any C Unit Testing Framework for testing on a host machine and also use specific C Unit Testing Frameworks for testing on a target machine with the appropriate target hardware.

Embedded Circuit
Image from Steven at Pixabay

If you are interested on how to do this and where the differences are when it comes to host and target you should read the rest of this article.

Is Modern Unit Testing possible in Embedded C?

Many people think that Unit Testing is not really possible when programming in an Embedded C environment. Due to it’s nature a lot of hardware is involved, such as circuit boards and corresponding peripherals. However this is absolutely not the case.

If you are not sure why you even should bother writing Unit Tests, you could this article that I wrote. It also gives a first introduction to Unit Testing Frameworks.

A lot of software consists of logic (sometimes called business logic) which is perfect for Unit Testing. The trick is to clearly define the boundaries between interfaces and modules. This way you can leave the hardware out, so to speak. This is usually done by a technique called mocking or stubbing.

To avoid misunderstandings: Unit testing in the context of this article is purely software related, not hardware related. There are other methods and options for hardware testing.

There are two options for you when it comes to Unit Testing Embedded C: You can test on your host (normally a Windows or Unix/Linux PC) and you can test on your target hardware (usually a circuit board with microprocessor). Testing on your host is fast and accelerates your developing process. But you should also test on your target machine from time to time, because different hardware, compilers or environments can alter the behaviour of your software.

You could also do Test Driven Development (TDD) which requires that you first write a test before you write any production code. TDD is not Unit Testing, it is a programming methodology. It’s advantage is the automatically high test coverage because you don’t forget to write a test. Nevertheless this is not mandatory. You can also write a piece of code and write the Unit Test afterwards, it’s perfectly fine.

How Do You Write a Unit Test for Embedded C?

First you will need a Unit Test Framework. It is possible to write first tests with native C code or create your own Framework as a programming exercise. But the best option is to use one of the available Frameworks that will do the job.

If you want to know what Unit Testing Frameworks there are for C and which one of them would be the best one for you, I wrote an article What is the Best Unit Testing Framework For You? that explains this topic in detail.

These Frameworks are good for testing on your host machine, but on your target machine you could lack memory or disk space for them. In this case there is a framework called JTN002 – MinUnit which consists of only 3(!) lines of code, so no room for excuses.

As for the hardware, on your host machine you will mock it or stub it. Instead of calling the real function of the hardware component or writing to a real bus, you would call a mocked function or write to virtual memory. The function would only serve the purposes of the test, so would the virtual memory.

Unit Test for Embedded C – Off-Target

Now we will examine a small example. I took parts of it from the book “Test Driven Development for Embedded C: Building High Quality Embedded Software” by James W. Grenning which is really excellent. If you want the complete example I highly recommend to read his book.

The goal is to program a driver for a small LED panel consisting of 8 LEDs. The tests I present here will make sure that all LEDs are off at startup and that we can turn on the first LED. We will only use virtual memory so no real hardware is needed.

For testing we will use the Unity test framework which is entirely written in C. You can download the framework hereOpens in a new tab..

Test Group and Tests

In order to group tests for features together we use Unity’s TEST_GROUP feature. You could setup and teardown the state needed for testing in the TEST_SETUP and TEST_TEARDOWN functions and it would automatically run before and after each test individually.

Our first test makes sure that all LEDs are off at startup of our LED driver. We create virtual memory for our LED panel and witch all LEDs on. This way we can see if the LEDs are really turned off by the controller instead of just being off by accident.

The second test let us turn on the first LED of the panel. We assert the memory with hexadecimal comparison, so the state with one LED turned on should be 0x00000001h.

#include "unity_fixture.h"

TEST_GROUP(LedDriver);

TEST_SETUP(LedDriver)
{
	/* Add Setup Code here */
}

TEST_TEAR_DOWN(LedDriver)
{
	/* Add Clean Up Code here*/
}

TEST(LedDriver, LedsOffAfterCreate)
{
	uint16_t virtualLeds = 0xffff;
	LedDriver_Create(&virtualLeds);
	TEST_ASSERT_EQUAL_HEX16(0, virtualLeds);
}

TEST(LedDriver, TurnOnLedOne)
{
	uint16_t virtualLeds;
	LedDriver_Create(&virtualLeds);
	LedDriver_TurnOn(1);
	TEST_ASSERT_EQUAL_HEX16(1, virtualLeds);
}

Implementation

The implementation to make our tests pass consists of two files. In the header file we simply declare our interface for our driver (as far as we tested it, of course it is not complete yet).

#pragma once
#include <stdint.h>

void LedDriver_Create(uint16_t address);
void LedDriver_TurnOn(const int ledNumber);

In the source file we define the interface functions and thus make the tests pass. We define our virtual memory, initialize it with 0 (hex) and turn the first LED on if requested, which means setting the virtual memory to 1 (hex).

#include "LedDriver.h"

static uint16_t* _ledsAddress;

void LedDriver_Create(uint16_t* address)
{
	_ledsAddress = address;
	*_ledsAddress = 0;
}

void LedDriver_TurnOn(const int ledNumber)
{
	*_ledsAddress = 1;
}

But wait… the implementation of LedDriver_TurnOn(…) looks wrong! That may be the case for a completely implemented LED driver, but in the TDD approach you only write enough code to make the test pass. From that standpoint the code is right. More tests will guide you eventually to the correct implementation (more on this topic in the book mentioned above).

Running the Tests

Before we can run the tests we have to tell Unity which tests it should run. Therefore we create a TestRunner.c file and create a TEST_GROUP_RUNNER. You have to make sure to add each new test here, otherwise it won’t be executed!

#include "unity_fixture.h"

TEST_GROUP_RUNNER(LedDriver)
{
	RUN_TEST_CASE(LedDriver, LedsOffAfterCreate);
	RUN_TEST_CASE(LedDriver, TurnOnLedOne);
}

Last but not least we need our main function. There we call Unity main and pass the run_all_tests function as a parameter. In this function we call our test runner.

#include "unity_fixture.h"

static void run_all_tests(void)
{
	RUN_TEST_GROUP(LedDriver);
}

int main(int argc, char** argv)
{
	return UnityMain(argc, argv, run_all_tests);
}

Now everything is setup and fine. When we compile and run the application we get the following output in the console:

LedDriver UnitTests Unity Results
Results of the Unity Test Runner

Unit Test for Embedded C – On-Target

Running a complete test framework on your target hardware is often not possible due to lacking hardware capabilities like memory and/or disk space. But there is a solution:

The minunit framework consists of 3(!) lines of code. The fourth line is the #pragma once declaration which is not really part of the framework. You simple put the following lines in a file called minunit.h and you are ready to use the framework.

#pragma once

#define mu_assert(message, test) do { if (!(test)) return message; } while (0)
#define mu_run_test(test) do { char *message = test(); tests_run++; if (message) return message; } while (0)
extern int tests_run;

When we take our LED_Driver example from above, the same test with the minunit framework would look like this:

Tests#pragma once

char* test_leddriver_create();
char* test_leddriver_turnon();
#include "LedDriverTestTarget.h"
#include "minunit.h"
#include "LedDriver.h"

char* test_leddriver_create()
{
	uint16_t virtualLeds = 0xffff;
	LedDriver_Create(&virtualLeds);
	mu_assert("error, virtualLeds != 0", 0 == virtualLeds);
	return 0;
}

char* test_leddriver_turnon()
{
	uint16_t virtualLeds;
	LedDriver_Create(&virtualLeds);
	LedDriver_TurnOn(1);
	mu_assert("error, virtualLeds != 1", 1 == virtualLeds);
	return 0;
}

The Implementation is (of course) exactly the same. But in order to run them on the target hardware you would have to write something like this into your program:

#include "minunit.h"
#include "LedDriverTestTarget.h"

int tests_run = 0;

static char* all_tests()
{
	mu_run_test(test_leddriver_create);
	mu_run_test(test_leddriver_turnon);
	return 0;
}

int main(int argc, char** argv)
{
	char* result = all_tests();
	if (result != 0) {
		printf("%s\n", result);
	}
	else {
		printf("ALL TESTS PASSED\n");
	}
	printf("Tests run: %d\n", tests_run);

	return result != 0;
}

The results (in the console, for example) would then look like this:

MinUnit: All Tests Passed
MinUnit: All Tests Passed

Logging without Display and Hard Drive

If you don’t have a display for your target hardware you could also use other periphery to debug and display test messages. You could use I2C like Jack Ganssle did in this videoOpens in a new tab., or you could use SPI like “Mikeselectricstuff” demonstrates it in this video.Opens in a new tab.

There are also dedicated Debugging devices but they are expensive and it is not always possible to connect them to your target hardware.

Marco Lieblang

Professional Programmer since 2003, passionate Programmer since the mid 90's. Developing in many languages from C/C++ to Java, C#, Python and some more. And I also may know a bit about Assembly Languages and Retro Systems.

Recent Posts