Unity

Unit Testing for C

(especially Embedded Software)

Unity is a Unit-Testing framework written in 100% pure ANSI C. It’s been carefully written and self-tested to be portable, working efficiently on tiny 8-bit microcontrollers to 64-bit powerhouses.

As a single C file and a pair of header files, Unity provides a slim but expressive collection of assertions and supporting structure, enabling you to write powerful unit tests.

Your C compiler. Your source code. Your tests. Unity allows you to test in C without littering your source code with additional requirements.

 
panel5.png

HOW UNITY WORKS

Unity is most obviously about assertions. Assertions are statements of what we expect to be true about our embedded system. At their most basic, they are things like this:

int a = 1;
TEST_ASSERT( a == 1 ); //this one will pass
TEST_ASSERT( a == 2 ); //this one will fail

You could use nothing but the TEST_ASSERT above, and you could test almost anything that your C code can handle... but when something went wrong, you'd see something like this:

TestMyModule.c:15:test_One:FAIL

While correct, it's not terribly informative. There are two ways to fix this. The brute force method (good for your non-standard corner cases):

TEST_ASSERT_MESSAGE( a == 2 , "a isn't 2, end of the world!");

which results in:

TestMyModule.c:15:test_one:FAIL:a isn't 2, end of the world!

And then there is the elegant solution, using Unity's multitude of pretty assertions:

TEST_ASSERT_EQUAL_INT(2, a);
TEST_ASSERT_EQUAL_HEX8(5, a);
TEST_ASSERT_EQUAL_UINT16(0x8000, a);

Which, if run in separate tests, would lead to the following failures:

TestMyModule.c:15:test_One:FAIL:Expected 2 was 1
TestMyModule.c:23:test_Two:FAIL:Expected 0x05 was 0x01
TestMyModule.c:31:test_Three:FAIL:Expected 32768 was 1

Isn't that nice? The first argument is the expected value. The second argument is the value you are testing. It's printed clearly in a format that is most convenient to you, the test writer. In fact, Unity can handle all sorts of types, not just integers.

TEST_ASSERT_EQUAL_FLOAT( 3.45, pi );
TEST_ASSERT_EQUAL_STRING( "Attention, Dr. Surly", greeting );

It can even handle situations where you want a custom message added, where you want to check a full array, or both!

TEST_ASSERT_EQUAL_INT_ARRAY( expArray, actualArray, numElements );
TEST_ASSERT_EQUAL_INT_MESSAGE( 5, val, "Not five? Not alive!" );
TEST_ASSERT_EQUAL_INT_ARRAY_MESSAGE( e, a, 20, "Oh snap!" );

One or more of these lovely assertions go into each test. A test is just a C function that takes no arguments and returns nothing. By convention, it starts with the word "test" or "spec":

void test_FunctionUnderTest_should_ReturnFive(void) {
TEST_ASSERT_EQUAL_INT( 5, FunctionUnderTest() );
TEST_ASSERT_EQUAL_INT( 5, FunctionUnderTest() ); //twice even!
}

A single test file will usually have multiple tests. Most often, one test file is used to test all aspects of a corresponding C source file. This can be made most clear with a simple naming convention: Where do you find the tests for MadScience.c? In TestMadScience.c, of course!

Get Unity

How you “get” Unity depends on how you plan to use it. Unity, at its core, is just a C file and a couple headers… but there are also optional feature files, support scripts, etc. Unity is meant to be built into a test executable for each unit. That means something needs to manage those builds. If that build management is going to be Ceedling, you’ve nothing to worry about. Installing Ceedling will automatically install Unity. Otherwise, you’re going to want to grab the source:

 
 

OR

 

Clone the Git Repo

git clone https://github.com/ThrowTheSwitch/Unity.git

git pull

panel4.png

Getting Started

The smallest realistic Unit Test build you can do is a source file, a test file, and Unity. Compile all three and link them together. We'll start with a native built app, because they're usually the simplest to get started. If you want to migrate to a simulated target later, we'll walk you through that too.

Let's say we have a C file that we want to test named DumbExample.c. It looks like this:

#include "DumbExample.h"

int8_t AverageThreeBytes(int8_t a, int8_t b, int8_t c)
{
  return (int8_t)(((int16_t)a + (int16_t)b + (int16_t)c) / 3);
}

It has a header file that looks like this:

#include <stdint.h>

int8_t AverageThreeBytes(int8_t a, int8_t b, int8_t c);

Then we make a test file TestDumbExample.c which checks for some basic things like rollovers and whatnot:

#include "unity.h"
#include "DumbExample.h"

void test_AverageThreeBytes_should_AverageMidRangeValues(void)
{
TEST_ASSERT_EQUAL_HEX8(40, AverageThreeBytes(30, 40, 50));
TEST_ASSERT_EQUAL_HEX8(40, AverageThreeBytes(10, 70, 40));
TEST_ASSERT_EQUAL_HEX8(33, AverageThreeBytes(33, 33, 33));
}

void test_AverageThreeBytes_should_AverageHighValues(void)
{
TEST_ASSERT_EQUAL_HEX8(80, AverageThreeBytes(70, 80, 90));
TEST_ASSERT_EQUAL_HEX8(127, AverageThreeBytes(127, 127, 127));
TEST_ASSERT_EQUAL_HEX8(84, AverageThreeBytes(0, 126, 126));
}

int main(void)
{
UNITY_BEGIN();
RUN_TEST(test_AverageThreeBytes_should_AverageMidRangeValues);
RUN_TEST(test_AverageThreeBytes_should_AverageHighValues);
return UNITY_END();
}

So we have a test file which contains two tests. Each test has multiple assertions. If any of those assertions fail, that particular test should fail and we should move on to the next test. When done, it should output our results.

Let's build and run executable! Assuming we have gcc installed, we can use it for the first step, and then directly run the binary produced

gcc TestDumbExample.c DumbExample.c ./unity/src/unity.c -o TestDumbExample
./TestDumbExample

or slightly differently on Windows:

gcc TestDumbExample.c DumbExample.c unity/src/unity.c -o TestDumbExample.exe
TestDumbExample

Either way, your command prompt should output something like this:

testUnit1.c:21:test_AverageThreeBytes_should_AverageMidRangeValues:PASS
testUnit1.c:22:test_AverageThreeBytes_should_AverageHighValues:PASS
-----------------------
2 Tests 0 Failures 0 Ignored
OK

SO that seemed to work. Of course, it's going to be very tedious to do all of this manually. We don't live in the stone age, here. We have tools! So, your next step is to determine what tools you want to use.