TDD in C

I do frontend AngularJS work for a client in Portland. One of the things I really like about their setup is its test-friendliness. They use Grunt to watch the client-side files and run tests and re-compile everything automatically when a change is detected. Everything gets wiped from the “build” directory when the process starts, and a failing test or JSLint warning blocks the whole process. This renders the developer’s copy of the web app inaccessible until the issue is addressed. I’ve found I enjoy this particular workflow as I’m immediately made aware of when I write something that breaks the tests. It keeps me from building on top of broken foundations.

This weekend I decided I wanted to sharpen my C skills by writing a simple CLI utility to convert strings of hexadecimal to memorable phrases and back again. Since my intended use case was encoding and recovering 256-bit private keys, I wanted to take extra care to ensure correctness of output. So I set out with the intention of writing both unit and user acceptance tests using a TDD flow similar to the one I use in my AngularJS work.

Unit Tests

One of the things I really like about C is its tininess. In light of this, and since I was just writing a small CLI utility to push bits around, I sought out the most minimal solution I could find. I decided to use MinUnit, which I modified a very little bit to more resemble KarmaJS’s syntax:

int tests_run = 0;  
#define _it_should(message, test) do { if (!(test)) return message; } while (0)  
#define _run_test(test) do { char *message = test(); tests_run++; if (message) return message; } while (0)  

minunit.h

This allowed me to write my test runner like this:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "minunit.h"  
#include "funcs.h"

static char * is_hex_01a() {  
    char hex[] = "0xB300562F8F9A961E158BDE2D4CCD2A64BB1D923208939714675BFFB17BBAF2A3";
    _it_should("return true for hex strings with leading '0x's", is_hex(hex));
    return 0;  
}

static char * run_tests() {  
    _run_test(is_hex_01a);
    return 0;  
}

#define KNRM "\x1B[0m"  
#define KRED "\x1B[1;37;41m"  
#define KGRN "\x1B[1;42;37m"

int main(int argc, char **argv) {  
    char *result = run_tests();  

    if (result != 0) {  
        printf(KRED "**FAIL**: %s" KNRM "\n", result);

    } else {  
        printf(KGRN "**PASSED ALL %d TESTS**" KNRM "\n", tests_run);  
    }  
}  

unit_tests.c

In all, I ended up writing 33 such tests. Running them was just a matter of compiling my tests.c file and then executing the resulting binary. I put together the beginnings of a Makefile to help with compilation:

#CC = gcc  
CFLAGS = -Wall -std=c99

tests: unit_tests.c funcs.h funcs.c  
    $(CC) $(CFLAGS) -o unit_tests unit_tests.c funcs.c

clean:  
    rm -f unit_tests *~ *.o core  

Makefile

The last piece for me was getting something in place to watch the file system and then kick off make tests and run the resulting binary. inotify- tools supplies a CLI utility called inotifywait that allows one to listen for filesystem events and blocks until a filesystem event is seen. As luck would have it, I was able to install it under Ubuntu with sudo apt-get install inotify-tools. I then wrote a script to take advantage of it:

run_tests() {  
    make -s clean  
    make -s tests  
    ./unit_tests  
}

if [ "$1" == "--watch" ] || [ "$1" == "-w" ]; then  
    while true; do  
        clear  
        run_tests  
        inotifywait -q -e close_write {*.c,*.h}  
    done  
else  
    run_tests  
fi  

unit_tests.sh

The above script is pretty simple. It just defines a function called run_tests that silently (-s) runs the clean and tests tasks in the Makefile I wrote, and then runs the unit_tests binary produced. If I pass --watch or -w as the first argument to the script, it clears the screen, runs the tests, and then waits until one of the .c or .h files in the project’s root is closed after being written to, and then repeats the whole process until the script is halted. If inotifywait is not on the developer’s system, or if one is using something like Travis-CI to run tests, executing the script with no flags just runs the tests and exits.

Now that I had a solid workflow for my unit tests in place, it was time to put together my user acceptance tests. This proved to be pretty simple as well.

User Acceptance Tests

Since user acceptance tests are all about testing the whole system, first I updated my Makefile to allow building the project within a subdirectory:

#CC = gcc  
CFLAGS = -Wall -std=c99  
BIN_DIR = ${DESTDIR}/usr/bin

portable: keyphrase.c funcs.h funcs.c tests  
    mkdir -p portable  
    cp words.txt portable  
    cp words.txt.gpg portable  
    $(CC) $(CFLAGS) -o portable/keyphrase keyphrase.c funcs.c

tests: unit_tests.c funcs.h funcs.c  
    $(CC) $(CFLAGS) -o unit_tests unit_tests.c funcs.c

clean:  
    rm -fr portable  
    rm -f keyphrase unit_tests *~ *.o core  

Makefile

Once I’d done that, I just put together a shell script to compile the utility into a subdirectory and check its output against what was expected.

failed_tests=0  
run_tests=0

it_should() {  
    if [ "$2" != "$3" ]  
    then  
        echo -e '\E[37;44m'"\033[1;37;41m**FAIL**: it should $1 \n          ↪ Expected '$2' but got '$3' \n\033[0m"  
        failed_tests=$((failed_tests + 1))  
    else  
        echo -e '\E[37;44m'"\033[1;42;37m**PASS**: it should $1 \033[0m"  
    fi  
    run_tests=$((run_tests + 1))  
}

test_key_to_phrase() {  
    actual=$(./portable/keyphrase 0xFFFF562F8F9A961E158BDE2D4CCD2A64BB1D923208939714675BFAB28BBAF2A3)  
    expected="zyzzyvas flutings mushers octopuses bizones talkier evokers coagent ringer neutral antipode omnibus havening whistles mistitled vacuums"  
    it_should "return the correct passphrase for a 256-bit key" "$expected" "$actual"  
}

run_tests() {  
    failed_tests=0  
    run_tests=0  
    make -s clean  
    make -s portable  
    test_key_to_phrase

    if [ "$failed_tests" -eq 0 ]; then  
        echo -e '\E[37;44m'"\033[1;42;37m**PASSED ALL $run_tests TESTS** \033[0m"  
    else  
        echo -e '\E[37;44m'"\033[1;37;41m**FAILED $failed_tests OUT OF $run_tests TESTS** \033[0m"  
    fi  
}

if [ "$1" == "--watch" ] || [ "$1" == "-w" ]; then  
    while true; do  
        clear  
        run_tests  
        inotifywait -q -e close_write {*.c,*.h}  
    done  
else  
    run_tests  
fi  

ua_tests.sh

I ended up writing four more tests using the same format as the example test shown above. With inotify-tools installed and the combination of C code and bash scripts outlined above, I had a pretty nice TDD workflow set up. The only issue was that sometimes ua_tests.sh and unit_tests.sh blocked each other by trying to access the same files at the same time. To resolve that issue, I wrote a third script to touch them off in serial:

run_tests() {  
    echo -e "\nUser Acceptance Tests\n====================="  
    ./ua_tests.sh

    echo "\nUnit Tests\n=========="  
    ./unit_tests.sh  
}

if [ "$1" == "--watch" ] || [ "$1" == "-w" ]; then  
    while true; do  
        clear  
        run_tests  
        change=$(inotifywait -q -e close_write,moved_to,create {*.c,*.h})  
    done  
else  
    run_tests  
fi

tests.sh

With that, I had a reliable testing workflow that kept me constantly updated on whether my C code was passing its tests or not. It felt a lot like my AngularJS workflow, and it helped me catch a lot of bugs the moment I wrote them rather than further down the line.

Later on, I also added desktop notifications to show me my test output using notify-send, a CLI utility that comes installed with Ubuntu. You can take a look at the later copies of ua_tests.sh and unit_tests.sh on Github if you want to see how I did that.

Any questions or suggestions about my TDD setup? Comments are always welcome!

Addendum: Apparently I could have done without maintaining a list of function calls to my tests. That would have streamlined my workflow even further!

Addendum 2: As someone on reddit pointed out to me, there are already a number of TDD frameworks for C, not the least of which are Check and TAP. If you’re planning on building anything bigger than what I’ve built here, you may want to use something more substantial than what I came up with.