DEV Community

Paul J. Lucas
Paul J. Lucas

Posted on

Writing and Running Unit Tests in Autotools

Introduction

If your project is like others, you’ll probably have a util.h and util.c files containing general utility functions. For example, suppose you want a function strnspn that’s like strspn() except limits the number of characters checked to n:

size_t strnspn( char const *s, char const *charset, size_t n );
Enter fullscreen mode Exit fullscreen mode

For such fundamental functions, you want to ensure they work correctly in all cases, including degenerate cases, so that if your program has a bug, you can reasonably rule out the bug being in one of those functions. To do that, you need to write unit tests for them.

A Simple Unit Unit Test Framework

In order to write unit tests, a small framework is helpful. Yes, I’m aware of this:

Standards

Specifically, there are several open-source, unit-test frameworks for C and they’re probably good; but I wanted something extremely simple. To that end, here’s unit_test.h:

// unit_test.h

#define TEST(EXPR)  ( !!(EXPR) || TEST_FAILED( #EXPR ) )

#define TEST_FAILED(EXPR) \
  ( fprintf( stderr, "%s:%d: " EXPR "\n", prog_name, __LINE__ ), \
    !++test_failures )

#define TEST_FUNC_BEGIN() \
  unsigned const test_failures_start = test_failures

#define TEST_FUNC_END() \
  return test_failures == test_failures_start
Enter fullscreen mode Exit fullscreen mode

To write unit tests, write one function for each “thing” you want to test where “thing” can be a single function or a few related functions:

static bool test_whatever( void ) {
  TEST_FUNC_BEGIN();

  TEST( whatever( ... ) ) == 42 );

  TEST_FUNC_END();
}
Enter fullscreen mode Exit fullscreen mode

If a test fails, the TEST macro will print the stringified expression that didn’t evaluate to true and its line number.

Note that the TEST macro also returns the success of the test so it can be used in a condition since there are some tests that depend on the success of earlier tests. For example, if you’re testing linked-list functions and you expect the front of the list to be the value “A,” you first want to test that the result isn’t NULL:

char const *const result = slist_front( list );
if ( TEST( result != NULL ) )
  TEST( strcmp( result, "A" ) == 0 );
Enter fullscreen mode Exit fullscreen mode

Similarly, a test function returns the success of the entire set of tests within the function in case other test functions depend on the success of earlier functions.

To write tests for strnspn, you’d create a file like:

// util_test.c

// local
#include "config.h"
#include "util.h"
#include "unit_test.h"

// standard
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>

char const *prog_name;
unsigned test_failures;

static bool test_strnspn( void ) {
  TEST_FUNC_BEGIN();

  TEST( strnspn( "", "AB", 0 ) == 0 );

  TEST( strnspn( "A",   "AB", 0 ) == 0 );
  TEST( strnspn( "B",   "AB", 0 ) == 0 );
  TEST( strnspn( "X",   "AB", 0 ) == 0 );

  TEST( strnspn( "XA",  "AB", 0 ) == 0 );
  TEST( strnspn( "XB",  "AB", 0 ) == 0 );
  TEST( strnspn( "XAB", "AB", 0 ) == 0 );

  TEST( strnspn( "A",   "AB", 1 ) == 1 );
  TEST( strnspn( "B",   "AB", 1 ) == 1 );

  TEST( strnspn( "AB",  "AB", 2 ) == 2 );
  TEST( strnspn( "BA",  "AB", 2 ) == 2 );

  TEST( strnspn( "ABA", "AB", 2 ) == 2 );
  TEST( strnspn( "ABX", "AB", 2 ) == 2 );

  TEST_FUNC_END();
}

int main( int argc, char const *const argv[] ) {
  prog_name = argv[0];
  test_strnspn();
  printf( "%u failures\n", test_failures );
  exit( test_failures > 0 );
}
Enter fullscreen mode Exit fullscreen mode

Integrating with Autoconf

To have Autoconf compile your test programs, add the following to src/Makefile.am:

check_PROGRAMS =    util_test

util_test_SOURCES = config.h unit_test.h util_test.c
Enter fullscreen mode Exit fullscreen mode

Then if you type make check, your test programs will be compiled at which point you can run them manually. To have make check also run the test programs automatically, also add the following to src/Makefile.am:

TESTS =             $(check_PROGRAMS)
Enter fullscreen mode Exit fullscreen mode

Conclusion

Any non-trivial project should include unit tests for all fundamental functions that can be tested independently using stand-alone executables. A simple testing framework helps write tests. Unit tests can be integrated with Autoconf to compile and run them.

There are more parts to come!

Top comments (0)