Simple unittesting in c

Category: c/c++ tips
Tags: C Unittests

Disclaimer

  1. I do way to less unittesting, so I an not the best person to promote this.

  2. YMMV, but I like this method because, it hits a sweet spot:

    • gtest is c++ only
    • c89spec has nice logging but chockes on async assert.
    • MinUnit has no nice logging, but is fantastic.

Background

Sometimes you are doing stuff in plain C, and you know that you should add some tests.

You also need to know what you'r doing, so you need to have some control. You also need something that you can use rightnow and that fits in your build system.

The setup

Your header 'magic.h'

file: magic.h

#ifndef groksome_magic_h_
#define groksome_magic_h_

int magic(const int magic);

#endif

Your code 'magic.c'

file: magic.h

#ifndef groksome_magic_h_
#define groksome_magic_h_

int magic(const int magic);

#endif

A simple header 'testrunner.h'

This is just boilerplate copy-pasta.

file: testrunner.h

#ifndef GROKSOME_TESTRUNNER_H_
#define GROKSOME_TESTRUNNER_H_

#include <stdio.h>
#include <time.h>

typedef struct _Testsuite_t {
    int run;
    int total;
    int asserts;
    FILE * logFh;
    } Testsuite_t;

#define MU_ASSERT( msg, test ) \
    do { printf("%05d |- %s\n", __LINE__, msg ); \
         if ( ! ( test ) ) { \
            message = msg; \
            goto cleanUp; \
          } \
         testsuite->asserts++; \
     } while (0)

#define MU_RUN_TEST( test, timelimit_sec ) \
    do { printf("%05d O--%s\n", __LINE__, # test ); \
         clock_t start = clock(); \
         char * message = run_ ## test( testsuite ); \
         clock_t end = clock(); \
         double elapsed_time = (end - start) / (double) CLOCKS_PER_SEC; \
         if (testsuite->logFh) { \
            fprintf(testsuite->logFh, "%s:%d\t %s %0.03f '%s'\n", __FILE__, __LINE__, #test, elapsed_time, (message) ? message : "ok"); \
         } \
         testsuite->run++; \
         if (! message && elapsed_time > timelimit_sec ) { \
            message = # test " took too long"; \
         } \
         if ( message ) { \
            return message; \
         } \
     } while ( 0 )

void        Testsuite_Init      ( Testsuite_t * testsuite, const char * logfile );
void        Testsuite_Clean     ( Testsuite_t * testsuite );
void        Testsuite_Report    ( Testsuite_t * testsuite, char * result );

#endif

A simple file 'testrunner.c'

This is just boilerplate copy-pasta.

file: testrunner.c

#include "testrunner.h"

#include <stdio.h>

void Testsuite_Init( Testsuite_t * testsuite, const char * filename ) {
    testsuite->run = 0;
    testsuite->total = 0;
    testsuite->asserts = 0;
    if ( filename ) {
        testsuite->logFh = fopen(filename, "a");
    }
}

void Testsuite_Clean( Testsuite_t * testsuite ) {
    testsuite->run = 0;
    testsuite->total = 0;
    testsuite->asserts = 0;
    if (testsuite->logFh) {
        fclose( testsuite->logFh);
    }
}

void Testsuite_Report( Testsuite_t * testsuite, char * result ) {
     if (result != NULL) {
         printf("      @# FAILED ON '%s'\n", result);
     } else {
         printf("     \\o/ All tests passed (%d asserts, %d runs)\n", testsuite->asserts, testsuite->run );
     }
}

Your test file 'magic_test.c'

4 lines from you, the rest is boilerplate.

file: magic.c

#include "magic.h"

int magic(const int magic) {
    return magic + 1;
}

As you can see, the test is very simple (Run_Magic_test):

  • As long as the 2nd parameter to 'MU_ASSERT' succeeds, the 'message' stays NULL and everything is fine.
  • If some test fails, it goes to the 'cleanup' label where you could free some alloced memory
  • The message will be forwarded to the testrunner.

The registration of new tests is also very simple, just with 'MU_RUN_TEST'. This has 2 parameters:

  • a name that will be expanded for the test function.
  • a maximum duration (ms) that the test may take

A Makefile for some clarity

file: Makefile.mk

CC ?= gcc
AR ?= ar
RANLIB ?= ranlib

TESTRUNNER_LIB=./tmp/testrunner.a

TARGETS = \
    ./tmp/magic_test \
#	./tmp/magic_main

all: $(TARGETS)

./tmp:
    mkdir -p $@

$(TESTRUNNER_LIB): ./tmp/testrunner.o ./tmp
    @$(AR) cru $@ $<
    @$(RANLIB) $@

./tmp/testrunner.o: testrunner.c testrunner.h ./tmp
    @$(CC) -c -o $@ $< -I.

./tmp/magic_test.o: magic_test.c magic.h ./tmp
    @$(CC) -c -o $@ $< -I.

./tmp/magic_test: ./tmp/magic_test.o ./tmp/magic.o $(TESTRUNNER_LIB)
    @$(CC) -o $@ $^

./tmp/magic.o: magic.c magic.h ./tmp
    @$(CC) -c -o $@ $<

./tmp/magic_main.o: magic_main.c magic.h ./tmp
    @$(CC) -c -o $@ $<

./tmp/magic_main: ./tmp/magic_main.o ./tmp/magic.o
    @$(CC) -o $@ $^

.PHONY: clean

clean:
    @rm -r *.o $(TARGETS) $(TESTRUNNER_LIB)

make -f Makefile.mk

The output

On success

00022 O--Magic_test
00012 |- return magic
     \o/ All tests passed (1 asserts, 1 runs)

On failure

00022 O--Magic_test
00012 |- return magic
      @# FAILED ON 'return magic'

and a logfile

magic_test.c:22	 Magic_test 0.000 'ok'
magic_test.c:22	 Magic_test 0.000 'return magic'

This makes it rather easy to add simple but scaleable unittests to your project.

It still looks like a awfull lot of boilerplate, but once you realise that adding a new test is just a few lines a way you'll love the flexibility.