Eric Radman : a Journal

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.