uCUnit: a Unit Test Framework for Microcontrollers
Want to learn more about unit testing for microcontrollers? Check out this post to learn more about uCUnit and unit testing for embedded development.
Join the DZone community and get the full member experience.
Join For FreeUnit testing is a common practice for host development. But for embedded development, this still seems mostly a ‘blank’ area. This is mostly because embedded engineers are not used to unit testing or because the usual framework for unit testing requires too many resources on an embedded target.
What I have used is the μCUnit framework, which is a small and easy-to-use framework, targeting small microcontroller applications.
The framework is very simple: two header files and a .c file:
Today, we will use the original ones from the uCUnit GitHub site or the ones I have modified from GitHub to be used with the MCUXpresso SDK and IDE.
The concept is that a unit test includes the uCunit.h header file, which provides test macros.
A #define in the header file configures the output as verbose or normal:
UCUNIT_MODE_NORMAL or UCUNIT_MODE_VERBOSE
System.c and System.h represents the connection to the system, which is basically used for startup, shutdown, and printing the test results to a console. Below shows an implementation using the printf()
method to write the output, but this could be replaced by any writing routine or extended to log text on an SD card.
/* Stub: Transmit a string to the host/debugger/simulator */
void System_WriteString(char * msg) {
PRINTF(msg);
}
void System_WriteInt(int n) {
PRINTF("%d", n);
}
Framework Overview
First, I have to include the unit test framework header file:
#include "uCUnit.h"
Then, I have to initialize the framework with:
UCUNIT_Init(); /* initialize framework */
One more test case is wrapped with a UCUNIT_TestcaseBegin()
and UCUNIT_TestcaseEnd()
:
UCUNIT_TestcaseBegin("Crazy Scientist");
/* test cases ... */
UCUNIT_TestcaseEnd();
To write a summary at the end, use
UCUNIT_WriteSummary();
And if the system shall be shut down, use a:
UCUNIT_Shutdown();
Tests
The framework provides multiple testing methods, such as:
UCUNIT_CheckIsEqual(x, 0); /* check if x == 0 */
UCUNIT_CheckIsInRange(x, 0, 10); /* check 0 <= x <= 10 */
UCUNIT_CheckIsBitSet(x, 7); /* check if bit 7 set */
UCUNIT_CheckIsBitClear(x, 7); /* check if bit 7 cleared */
UCUNIT_CheckIs8Bit(x); /* check if not larger then 8 bit */
UCUNIT_CheckIs16Bit(x); /* check if not larger then 16 bit */
UCUNIT_CheckIs32Bit(x); /* check if not larger then 32 bit */
UCUNIT_CheckIsNull(p); /* check if p == NULL */
UCUNIT_CheckIsNotNull(s); /* check if p != NULL */
UCUNIT_Check((*s)==’\0’, "Missing termination", "s"); /* generic check: condition, msg, args */
This is explained best with a few examples. Let's take a look.
Example: Crazy Scientist
Below is a ‘crazyScientist’ function that combines different materials:
typedef enum {
Unknown, /* first, generic item */
Hydrogen, /* H */
Helium, /* He */
Oxygen, /* O */
Oxygen2, /* O2 */
Water, /* H2O */
ChemLast /* last, sentinel */
} Chem_t;
Chem_t crazyScientist(Chem_t a, Chem_t b) {
if (a==Oxygen && b==Oxygen) {
return Oxygen2;
}
if (a==Hydrogen && b==Oxygen2) {
return Water;
}
return Unknown;
}
A test for this could look like this:
void Test(void) {
Chem_t res;
UCUNIT_Init(); /* initialize framework */
UCUNIT_TestcaseBegin("Crazy Scientist");
res = crazyScientist(Oxygen, Oxygen);
UCUNIT_CheckIsEqual(res, Oxygen2);
UCUNIT_CheckIsEqual(Unknown, crazyScientist(Water, Helium));
UCUNIT_CheckIsEqual(Water, crazyScientist(Hydrogen, Oxygen2));
UCUNIT_CheckIsEqual(Water, crazyScientist(Oxygen2, Hydrogen));
UCUNIT_CheckIsInRange(crazyScientist(Unknown, Unknown), Unknown, ChemLast);
UCUNIT_TestcaseEnd();
/* finish all the tests */
UCUNIT_WriteSummary();
UCUNIT_Shutdown();
}
With the different checks, we can verify if the function is doing what we expect. It produces the following output:
======================================
Crazy Scientist
======================================
../source/Application.c:60: passed:IsEqual(res,Oxygen2)
../source/Application.c:61: passed:IsEqual(Unknown,crazyScientist(Water, Helium))
../source/Application.c:62: passed:IsEqual(Water,crazyScientist(Hydrogen, Oxygen2))
../source/Application.c:63: failed:IsEqual(Water,crazyScientist(Oxygen2, Hydrogen))
../source/Application.c:64: passed:IsInRange(crazyScientist(Unknown, Unknown),Unknown,ChemLast)
======================================
../source/Application.c:65: failed:EndTestcase()
======================================
**************************************
Testcases: failed: 1
passed: 0
Checks: failed: 1
passed: 4
**************************************
System shutdown.
I recommend writing the unit tests *before* doing the implementation, because this way, it lets me consider all the different corner cases and refine the requirements.
The above output is with UCUNIT_MODE_VERBOSE set. Using UCUNIT_MODE_NORMAL, it uses a more compact format and prints the failed tests only:
======================================
Crazy Scientist
======================================
../source/Application.c:63: failed:IsEqual(Water,crazyScientist(Oxygen2, Hydrogen))
======================================
../source/Application.c:65: failed:EndTestcase()
======================================
**************************************
Testcases: failed: 1
passed: 0
Checks: failed: 1
passed: 4
**************************************
System shutdown.
Trace Points
In the above example, we were only testing from the outside what the function does.
How to check that the following function can be tested and, indeed, checks for the division by zero cases? Let's take a look:
int checkedDivide(int a, int b) {
if (b==0) {
PRINTF("division by zero is not defined!\n");
return 0;
}
return a/b;
}
To check that the if()
condition has been really entered, I can add a tracepoint. The number of tracepoints is configured in μCUnit.h with:
/**
* Max. number of checkpoints. This may depend on your application
* or limited by your RAM.
*/
#define UCUNIT_MAX_TRACEPOINTS 16
With;
UCUNIT_ResetTracepointCoverage();
I can reset the tracepoints by marking the execution of a tracepoint with an id (which is in the range 0..UCUNIT_MAX_TRACEPOINTS-1):
UCUNIT_Tracepoint(id);
With:
UCUNIT_CheckTracepointCoverage(0);
Also, I can check if a given tracepoint has been touched. Below the function to be tested instrumented with a tracepoint:
int checkedDivide(int a, int b) {
if (b==0) {
UCUNIT_Tracepoint(0); /* mark trace point */
PRINTF("division by zero is not defined!\n");
return 0;
}
return a/b;
}
The corresponding unit test code:
UCUNIT_TestcaseBegin("Checked Divide");
UCUNIT_CheckIsEqual(100/5, checkedDivide(100,5));
UCUNIT_ResetTracepointCoverage(); /* start tracking */
UCUNIT_CheckIsEqual(0, checkedDivide(1024,0));
UCUNIT_CheckTracepointCoverage(0); /* check coverage of point 0 */
UCUNIT_TestcaseEnd();
Which then produces:
======================================
Checked Divide
======================================
../source/Application.c:69: passed:IsEqual(100/5,checkedDivide(100,5))
division by zero is not defined!
../source/Application.c:71: passed:IsEqual(0,checkedDivide(1024,0))
../source/Application.c:72: passed:TracepointCoverage(1)
String Test
There are many other ways to use checks and have user configured checks and messages. Below is an example of a function to test:
char *endOfString(char *str) {
if (str==NULL) {
return NULL;
}
while(*str!='\0') {
str++;
}
return str;
}
With the following test code:
UCUNIT_TestcaseBegin("Strings");
UCUNIT_CheckIsNull(endOfString(NULL));
str = endOfString("abc");
UCUNIT_Check(
(str!=NULL), /* condition to check */
"string shall be not NULL", /* message */
"str" /* argument as string */
);
UCUNIT_CheckIsEqual('\0', *endOfString(""));
UCUNIT_CheckIsEqual('\0', *endOfString("hello"));
str = endOfString("world");
UCUNIT_CheckIsNotNull(str);
UCUNIT_CheckIsEqual('\0', *str);
UCUNIT_TestcaseEnd();
This produces:
======================================
Strings
======================================
../source/Application.c:76: passed:IsNull(endOfString(NULL))
../source/Application.c:82: passed:string shall be not NULL(str)
../source/Application.c:83: passed:IsEqual('\0',*endOfString(""))
../source/Application.c:84: passed:IsEqual('\0',*endOfString("hello"))
../source/Application.c:86: passed:IsNotNull(str)
../source/Application.c:87: passed:IsEqual('\0',*str)
Summary
μCUnit is a very simple yet powerful unit testing framework for embedded devices and microcontrollers. It is easy to use and only requires minimal resources and helps to increase the quality of embedded software with automated unit tests. I hope you find it useful, too!
Happy testing!
Helpful Links
- μCUnit web page: http://www.ucunit.org/
- μCUnit Documentation: http://www.ucunit.org/_documentation.html
- μCUnit Github site: https://github.com/ucunit/ucunit
- μCUnit example usage: https://github.com/ErichStyger/mcuoneclipse/tree/master/Examples/MCUXpresso/FRDM-K64F/FRDM-K64F_uCUnit
- Port of μCUnit for MCUXpresso: https://github.com/ErichStyger/mcuoneclipse/tree/master/Examples/MCUXpresso/FRDM-K64F/FRDM-K64F_uCUnit/uCUnit
Published at DZone with permission of Erich Styger, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments