Unit testing is something that lots of developers are used to, and in environments like Python or Java can be even bundled in some awesome libraries. Easy to use, easy to implement, great results.
Then it comes C Embedded: The environment is different, even the people is different!!. Praising the benefits of unit testing is not that easy sometimes, even writing the unit test functions is not as straigtforward.
In this post, I’ll talk about Unity, a small unit test library for C.
Why Unity?
There are a lot of unit test “frameworks” or “libraries” for C.
Its selling points, from the same website:
- Plain C
- portable
- expressive
- Quick
- Simple
- Extensible
It’s always funny how people throw adjectives like crazy to a product. I want to add a note to simple. Unity is simple to understand, nevertheless the tests grow convoluted sometimes.
Since there’s a big videogames IDE named unity, finding specific information of this unit test library via search engine is sometimes difficult 😛
Only Unity?
Well. Unity by itself is a set of MACROS and FUNCTIONS with pretty printing of assertions and error/pass counts.
It works, and it’s quite direct. Also, even if I have not tried it yet, you could theorically build a simple test with a cross-compiler and run it in your hardware, printing results via UART.
At the end of the day, you’ll end up coupling with stuff like CMock, a Makefile (Rakefile, cmakefile or whatever). And once the mocks kick in, it can get strange and cause fear like Cthulhu. Just look at the Makefile documentation for unity. fortunately the guys at throw the switch are as “special” as we are, and their documentations and courses are fun to read/listen.
Pointing to a ruby build system, is not incidental
A basic let’s go!
On a simple non-dependant module, Unity shines. Include the library, write some simple tests and go. I present here an example of a weird adding function.
/**@brief This function adds two numbers and sets the error variable to 1 if overflow happened */ uint32_t myWeirdAdd(uint32_t a, uint32_t b, uint8_t* error) { *error = (a > UINT32_MAX - b); return a + b; }
We’ll want to test two things. Basic functionality (Addition of two integers), and overflow. Let’s see how we would do that with Unity:
#include "unity.h" #include "func.h" /**@brief test that basic add works, and returns error code of 0*/ void test_BasicAdditionShouldWork(void) { uint8_t err; TEST_ASSERT_EQUAL(30, myWeirdAdd(10, 20, &err)); TEST_ASSERT_EQUAL(0, err); } /**@brief test that the overflow is detected, and returns error code of 1*/ void test_BasicOverflowIsDetected(void) { uint8_t err; myWeirdAdd(0xFFFFFFFF, 20, &err); TEST_ASSERT_EQUAL(1, err); } /**@brief Main program to be executed*/ int main(void) { UNITY_BEGIN(); RUN_TEST(test_BasicAdditionShouldWork); RUN_TEST(test_BasicOverflowIsDetected); return UNITY_END(); }
This is as simple as it gets:
The test functions begin with test_
by convention, we can name it as we like though.
Each test runs the function with different parameters, and ASSERTS
that the output is the one that we expect. This special asserts are the ones that will generate a nice stdout
output on the command line, once we have a lot of tests that output is very valuable.
Once compiled, we’ll have our test ready:
gcc unity.c func.c test.c -o test
./test
$ ./a.exe test.c:26:test_BasicAdditionShouldWork:PASS test.c:27:test_BasicOverflowIsDetected:PASS ----------------------- 2 Tests 0 Failures 0 Ignored OK
Conclusions
In this post I’m guilty of the basic sin of “the easy example”. Over my time as an engineer I’ve seen a lot of examples on books and websites that go like this:
- Let’s start with building a wheel
- here you can see an example of building a wheel
- Now you know how to build a wheel, go and build your own car!
The basic step is always easy, the devil is in the details :-D. So I have no excuse. The difficult part of the unit testing (not taking into account the design of the tests themselves) in C is two-fold:
- Mocks
- Pointers!!
Mocking is an interesting part by itself. The main idea is to control dependencies between modules by creating a dummy module. That module will simply check that is called under specific circumstances. But of course, you have to tell it what circumstances. With all that, you end with long tests with lots of boilerplate things. All this boilerplate is actually checking that everything goes fine and dandy, but it’s not as clear as the TEST_ASSERT
used in my example.
Check this longer example using Cmock and Unity.
void test_projXsetStuff_normalFlow(void) { uint8_t setStuff, retVal; uint8_t dataSizeReturn; dispatcherFunction function = (dispatcherFunction)projXstuffFunction; setStuff = 1; //Assume that our GPIO is ready to go //Note that this is a MACRO, hiding a complexity of 4 functions underneath TEST_CMOCK_READ_REG_EXPECT(GPIO_ISGOODTOGO, GPIO_VALUE, 1, 1); //Check that timer has expired tmrIsExpired_ExpectAndReturn(TMR_2, TRUE); //finally the stuff will be set to one TEST_CMOCK_WRITE_REG(GPIO_STUFF, GPIO_VALUE, 1); //TMR_1 keeps STUFF on tmrSetCallbackFunction_ExpectAndReturn(TMR_1, NULL, NOERROR); tmrSetCallbackFunction_IgnoreArg_callbackFunction(); tmrSetTimeout_ExpectAndReturn(TMR_1, TMR_1_TIMEOUT_VAL, NOERROR); tmrResetTimer_ExpectAndReturn(TMR_1, NOERROR); tmrStartTimer_ExpectAndReturn(TMR_1, NOERROR); //TMR_2 counts how long before STUFF can be ReActivated tmrSetTimeout_ExpectAndReturn(TMR2, TMR_2_TIMEOUT_VAL, NOERROR); tmrResetTimer_ExpectAndReturn(TMR2, NOERROR); tmrStartTimer_ExpectAndReturn(TMR2, NOERROR); //Another helper function. Filling an input buffer with what is expected fillInBuffer(1, setStuff); retVal = function(dataBufferIn, dataBufferOut, &dataSizeReturn); TEST_ASSERT_EQUAL(0, retVal); //No error happened TEST_ASSERT_EQUAL(0, dataSizeReturn); //The return data size payload is zero }
Pointers are in general more or less gracefully handled by Cmock. But as far as I’ve seen it’s a source of confusion with test writers and readers alike.
Closing comments? Unit testing C is possible, but you need: a good infrastructure, a good project organization, an easy to use build process (+ at least somebody understanding all the runes involved), good module isolation (damn, that’s not the common thing in the Embedded projects where I landed), and the stars aligned. It may be a slow process, but the payback can be worth it.
I say so from the point of view of a guy that used Python. You don’t need anything special, unit test is there free, and it matches easily with OO design. Which is not the case with C.