– Boris Beizer
Some programmers are definitely hard to convince that unit testing is a good idea. Especially die-hard embedded C coders go to great lengths to “prove” that for their project, unit testing is unthinkable.
One frequently presented argument against unit testing is that the project is real-time and/or deeply embedded and that the code is so low-level and hardware-dependent that it’s impossible to unit test it.
To me, this argument doesn’t hold water. You just need to introduce the right abstractions. Obviously, you can’t unit-test the precision of a timer with a one microsecond resolution, still, you can test against its (mocked) interface. And you can definitely test the effects of an expired timer, when an “timer expired” callback method is executed. It’s just a matter of isolating hardware (or in general, things that are difficult to unit test) from business logic. If the abstractions are right, unit testing is not just possible, it’s a breeze! As a bonus, you usually get lots of portability and reuse for free.
Another excuse for not unit testing is lack of time. “We can’t do unit testing because our schedule is so tight. Boohoo!”. That’s a fake lament, too. Granted, implementation might initially take longer, but implementation isn’t everything: software integration and system testing contribute major parts to the overall schedule, and both phases can be significantly reduced by employing unit testing, as many studies have demonstrated (refer to Roy Osherove’s book “The Art of Unit Testing, 2nd Edition”, for instance). The later you find a bug, the more expensive it is to fix. And while unit testing obviously can’t find all bugs it finds a lot of bugs and it finds them early in the development life-cycle.
In order to lead by example, I’m going to do something outrageous: I’m going to unit-test one of the most simple pieces of software ever written, the famous “Hello, World!” program. Even though it might sound crazy, I chose “Hello, World” because most developers would think that it’s a) impossible to unit-test and b) hard to get wrong in the first place.
Let’s start by taking a look at the venerable implementation by Kernighan and Ritchie as shown in their famous “The C Programming Language” book:
1 2 3 4 5 6 7 8 |
#include <stdio.h> main() { printf("hello, world\n"); } |
Actually, “Hello World” does two things: apart from printing the famous words, it (implicitly) returns an exit code of zero to the operating system. My first refactoring makes the original version’s behavior more explicit:
1 2 3 4 5 6 |
int main() { printf("hello, world\n"); return 0; } |
Since calling main from a unit test is not possible, let’s do a another quick modification and encapsulate the “Hello World” functionality in a function of its own:
1 2 3 4 5 6 7 8 9 10 |
int hello_world() { printf("hello, world\n"); return 0; } int main() { return hello_world(); } |
Now, if this isn’t an example of “Design for Testability”, I don’t know what is ;-) Contrary to the original code, this new design allows calling/observing the behavior from a unit test. While verifying the return code is easy, ensuring that the right string is printed is a bit more involved and requires some creativity.
My initial idea was to redirect stdout to a file during the test and later check if the file contents are what I expect:
1 2 3 4 5 6 7 8 9 10 |
const char* const TEST_FILE = "/tmp/test_hello_world_output.txt"; if (freopen(TEST_FILE, "w", stdout) != NULL) { int exit_code = hello_world(); fclose(stdout); assert(exit_code == 0); assert(file_content_matches_string(TEST_FILE, "hello, world")); } |
Even though this approach works, I rather prefer the more traditional mock-based approach, which uses the preprocessor to redirect calls to printf to a mocked version of printf:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
#define printf mock_printf static size_t mock_printf_calls = 0; static const char* mock_printf_arg1 = NULL; static int mock_printf_return = 0; void mock_printf_reset() { mock_printf_calls = 0; mock_printf_arg1 = NULL; mock_printf_return = 0; } int mock_printf(const char *__restrict text, ...) { ++mock_printf_calls; mock_printf_arg1 = text; return mock_printf_return; } |
The test itself is straightforward:
1 2 3 4 5 6 7 |
mock_printf_reset(); int ret = hello_world(); assert(ret == 0); assert(mock_printf_calls == 1); assert(strcmp(mock_printf_arg1, "hello, world!\n") == 0); |
As you can see, it’s quite simple to create your own mocks, even in plain old C, where no fancy mocking frameworks are available.
But of course, unit testing is impossible if your insist on writing monolithic code with functions comprising 100+ lines that directly do I/O or manipulate device registers. It’s also impossible if you prefer delving through bug reports from field testing when you instead could have found and fixed that off-by-one error from the comfort of your home office, ten minutes after you introduced it.
Unit testing is no panacea, it’s by no means a substitute for system testing — it’s yet another rung on the software quality ladder. Let’s be honest: the real reason for not doing unit testing is not a technical one — it’s pure laziness.
(You can find the source code, tests and makefiles here.)