new generation unit test framework for C
First, download a release tarball.
Novaprova is intended to be built in the usual way that any open source C software is built. However at the moment it doesn't even use autotools, so there's no configure script. To build you need to have g++ and gcc installed. You will also need to have the valgrind.h header file installed, which is typically in a package named something like valgrind-dev .
# download the release tarball from http://sourceforge.net/projects/novaprova/files/ tar -xvf novaprova-0.1.tar.bz2 cd novaprova-0.1 make make install
For advanced users only. Novaprova needs several more tools to build from a Git checkout than from a release tarball, mainly for the documentation. You will need to have Python Markdown, Pygments, and Doxygen installed. On an Ubuntu system these are in packages python-markdown, python-pygments, and doxygen respectively.
git clone git://github.com/gnb/novaprova.git
cd novaprova
make
make install
Because you're testing C code, the first step is to build a test runner executable. This executable will contain all your tests, the Code Under Test and a small main routine, and will be linked against the NovaProva library and whatever other libraries your Code Under Test needs. Typically, this is done using the check: make target to both build and run the tests.
# Makefile check: testrunner ./testrunner TEST_SOURCE= mytest.c TEST_OBJS= $(TEST_SOURCE:.c=.o) testrunner: testrunner.o $(TEST_OBJS) libmycode.a $(LINK.c) -o $@ testrunner.o $(TEST_OBJS) libmycode.a $(NOVAPROVA_LIBS)
NovaProva uses the GNOME pkgconfig system to make it easy to find the right set of compile and link flags.
NOVAPROVA_CFLAGS= $(shell pkg-config --cflags novaprova) NOVAPROVA_LIBS= $(shell pkg-config --libs novaprova) CFLAGS= ... -g $(NOVAPROVA_CFLAGS) ...
Note that you only need to compile the test code mytest.c and the test runner code testrunner.c with NOVAPROVA_CFLAGS. NovaProva does not use any magical compile options or do any pre-processing of test code.
However, you should make sure that at least the test code is built with the -g option to include debugging information. NovaProva uses that information to discover tests at runtime.
Your main routine in testrunner.c will initialise the NovaProva library by calling np_init and then call np_run_tests to do all the heavy lifting. Here's a minimal main.
/* testrunner.c */ #include <stdlib.h> #include <unistd.h> #include <np.h> /* NovaProva library */ int main(int argc, char **argv) { int ec = 0; np_runner_t *runner; /* Initialise the NovaProva library */ runner = np_init(); /* Run all the discovered tests */ ec = np_run_tests(runner, NULL); /* Shut down the NovaProva library */ np_done(runner); exit(ec); }
The last piece of the puzzle is writing some tests. Each test is a single C function which takes no parameters and returns void. Unlike other unit test frameworks, there's no API to call or magical macro to use to register tests with the library. Instead you just name the function test_something, and NovaProva will automatically create a test called something which calls the function.
For example, let's create a test called simple which tests the function myatoi which has the same signature and semantics as the well-known atoi function in the standard C library.
/* mytest.c */ #include <np.h> /* NovaProva library */ #include "mycode.h" /* declares the Code Under Test */ static void test_simple(void) { int r; r = myatoi("42"); NP_ASSERT_EQUAL(r, 42); }
The macro NP_ASSERT_EQUAL checks that it's two integer arguments are equal, and if not fails the test. Note that if the assert fails, the test function terminates immediately. If the test function gets to it's end and returns naturally, the test is considered to have passed.
If we build run this test we get output something like this.
% make check
./testrunner
np: starting valgrind
np: running
np: running: "1:simple"
PASS 1:simple
np: 1 run 0 failed
As expected, the test passed. Now let's add another test. The myatoi function is supposed to convert the initial numeric part of the argument string, i.e. to stop when it sees a non-numeric character. Let's feed it a string which will exercise this behaviour and see what happens.
static void test_initial(void) { int r; r = myatoi("4=2"); NP_ASSERT_EQUAL(r, 4); }
Running the tests we see:
% make check ./testrunner np: starting valgrind np: running np: running: "1:mytest.simple" PASS 1:mytest.simple np: running: "2:mytest.initial" EVENT ASSERT NP_ASSERT_EQUAL(r=532, 4=4) at 0x80529F2: np::spiegel::describe_stacktrace (np/spiegel/spiegel.cxx) by 0x804C0FC: np::event_t::with_stack (np/event.cxx) by 0x804B2D2: __np_assert_failed (uasserts.c) by 0x804AC27: test_initial (mytest.c) by 0x80522D0: np::spiegel::function_t::invoke (np/spiegel/spiegel.cxx) by 0x804C731: np::runner_t::run_function (np/runner.cxx) by 0x804D5C4: np::runner_t::run_test_code (np/runner.cxx) by 0x804D831: np::runner_t::begin_job (np/runner.cxx) by 0x804E0D4: np::runner_t::run_tests (np/runner.cxx) by 0x804E22C: np_run_tests (np/runner.cxx) by 0x804AB12: main (testrunner.c) FAIL 2:mytest.initial np: 2 run 1 failed make: *** [check] Error 1
The first thing we see is that the name of the old test has changed from simple to mytest.simple. NovaProva organises tests into a tree whose node names are derived from the test source directory, test source filename, and test function name. This tree is pruned down to the smallest possible size at which the root of the tree is unique. So when we added a second test in the same source file, the full name of both tests now includes a component mytest derived from the name of the source file mytest.c.
Note also that the new test failed. Immediately after the "np: running:" message we see that the NP_ASSERT_EQUAL macro has failed, and printed both its arguments as well as a stack trace. We expected the variable r to equal to 4 but its actual value at runtime was 532; clearly the myatoi function did not behave correctly. We found a bug!