Test-Driven Development in C
Writing a test harness for programs written in C requires some scaffolding, but I agree with Theo de Raadt is correct when he observed that heavy frameworks get in the way.
These are some techniques for building a test runner which is capable of mocking individual function calls.
A Simple Test Runner
Start by including the header file of the module or system under test. The following is based on MinUnit, a minimal unit testing framework for C by John Brewer
/* runner.c */ #include <stdio.h> #include "sut.h" int tests_run = 0;
Next define some simple macros that will print a meaningful error and return
1
if the test condition is not true
#define FAIL() printf("\nfailure in %s() line %d\n", __func__, __LINE__) #define _assert(test) do { if (!(test)) { FAIL(); return 1; } } while(0) #define _verify(test) do { int r=test(); tests_run++; if(r) return r; } while(0)
do { ... } while(0)
is a convention for including multiple statements in a macro; the block
gives variable declarations local storage.
The test runner itself is responsible for calling the
run_test
macro, run each test in turn until one fails.
int square_01() { int x=5; _assert(square(x) == 25); return 0; } int all_tests() { _verify(square_01); return 0; } int main(int argc, char **argv) { int result = all_tests(); if (result == 0) printf("PASSED\n"); printf("Tests run: %d\n", tests_run); return result != 0;
Link-level Stubbing
Among the techniques available for faking functions in C, link-level stubs are
a great place to begin because they require no changes to production code.
One of the first functions to replace is
main()
so that the test runner is the new entry point for the resulting binary.
runner: runner.o sut.o → strip -N main sut.o _sut.o → ${CC} -o $@ runner.o _sut.o ${LDFLAGS}
Here we create a stripped version of the object code
utility.o
which allows the linker to connect the new main from
runner.o
.
Ideally tesrunner is executed as part of the build.
all: utility runner test test: → ./runner
Faking Interfaces with the C Preprocessor
The most straitfoward way to inject test code into each module is with the C preprocessor. This is a very flexible technique, although it may require separate makefile to generate the test build.
Everything needs to be built twice, so it really helps to use a fast compiler. Build scripts and makefiles are making very strategic substitutions here, so I would strongly recommend staying clear of auto-tools. Instead factor out common elements and include them in platform-specific build configurations.
sut.o: → @echo CC $< → @${CC} -c -DTEST ${CFLAGS} $<
Preprocessor instructions can be placed anywhere, but one way of organizing
code is to break it into two distinct segments. The first is the system under
test, which contain all of the core functionality of the module, the second
will include
main()
and other definitions that may be replaced by fake functions in the
specification header.
/* functions to be tested */ #ifdef TEST #include "sut_spec.h" #else /* functions to be faked */ #endif
Global Function Pointers
Functions can be aliased with a function pointer which the test runner can then redefine at runtime. This is the approach I took when writing unit tests for entr
#include <stdlib.h> /* function pointers */ char * (*xrealpath)(const char *, char *); int main() { xrealpath = realpath; }
Instead of assigning the real function, the test runner can stub the result
#include "main.c" char * fake_realpath(const char *pathname, char *resolved) { snprintf(resolved, PATH_MAX, "/home/user/%s", pathname); return resolved; } int test_main(int argc, char *argv[]) { xrealpath = fake_realpath; }
Toy Programs
Ten years after writing this post on techniques for mocking function in C, I have adopted a different tact, which is to write small programs that exercise parts of a larger library. This is the pattern employed by the test harness for rset
#include <stdio.h> #include "rutils.h" int main(int argc, char *argv[]) { if (argc != 3) { fprintf(stderr, "usage: ./copyfile src dst\n"); return 1; } install_if_new(argv[1], argv[2]); return 0; }
These utilities can then be run in a temporary environment using an external test framework written in a scripting language.