Rexo: A Unit Testing Framework in C

In the serie of reinventing the wheel for the sake of learning, let me introduce you to the unit testing framework!

Update

A full redesign of Rexo has since been released, see Rexo: A Unit Testing Framework in C (Part 2).

RexoView on GitHub

Test Registration

While looking around for unit testing frameworks written in C, I noticed that the vast majority were fairly verbose and didn’t provide any sort of automatic test registration like most of their C++ counterparts do.

There had to be some sort of additional constraints inheritent to C, but which ones? In an effort to learn more about the C language, I set out to answer this question by writing my own unit testing framework from scratch, in C89.

The reason why automatic test registration is not available in C quickly became obvious. C++ frameworks like Google Test use some clever static initialization trickery that registers each test to a global factory class with the help of some macros to nicely hide the registration logic away from the user[1].

But no function, not even static, can be called at the file scope in C. All that is allowed is defining simple constant values and functions, so no such trickery allowed here.

As an example, the simple code snippet below works perfectly fine in C++ but triggers a compilation error in C.

#include <stdio.h>

static int
foo()
{
    return 123;
}

static const int bar = foo();

int
main()
{
    printf("bar is %d\n", bar);
    return 0;
}

Some C frameworks such as NovaProva workaround this limitation by reading the DWARF debug information generated during a debug compilation while Criterion parses the generated executable file formats to read the contents of specific data sections.

I wasn’t ready to dig into such involved approaches so I picked the more standard route with this first iteration of Rexo and went for the manual registration choice with all the verbosity that it comes with.

API Design

Rexo provides both a diagonal API for ease of use and an orthogonal one for maximum customizability.

The diagonal API is just one function, rxRun(). It takes the suites as parameter and run them.

The orthogonal API is made of all the smaller logical parts that rxRun() uses to get the job done, that is rxInitializeTestCaseReport(), rxTerminateTestCaseReport(), rxRunTestCase(), and rxPrintTestCaseRunSummary(). If you go down this path, it means that you need to write your own function to run the tests, and it’s also up to you to use the provided orthogonal functions or here again to write your own variants... but at this point, you’re probably better off writing your own framework from scratch.

Simple Usage

Here’s a basic example on how to use Rexo, with a fixture and a display of a few of the available assertion macros, which are declined in CHECK and REQUIRE variants, respectively for nonfatal and fatal failures.

#define RX_DEBUG_LOGGING_LEVEL
#include <rexo/rexo.h>

#include <stddef.h>
#include <stdio.h>

#define PI 3.14159265358979323846

struct Fixture {
    int zero;
    unsigned int one;
};

enum RxStatus
setUp(void **ppFixture)
{
    struct Fixture *pData;

    pData = (struct Fixture *)malloc(sizeof *pData);
    if (pData == NULL) {
        return RX_ERROR_ALLOCATION;
    }

    pData->zero = 0;
    pData->one = 1;

    *ppFixture = (void *)pData;
    return RX_SUCCESS;
}

void
tearDown(void *pFixture)
{
    free(pFixture);
}

RX_TEST_CASE(testBasics)
{
    struct Fixture *pData;

    pData = (struct Fixture *)RX_FIXTURE;

    RX_CHECK_INT_EQUAL(pData->zero, 0);
    RX_CHECK_UINT_EQUAL(pData->one, 1);
    RX_CHECK_FP_ALMOST_EQUAL(sin(PI), 0.0, 1.0e-6);
    RX_CHECK_FP_ALMOST_EQUAL(sin(PI * 0.5), 1.0, 1.0e-6);
    RX_CHECK_STRING_EQUAL_NO_CASE("abc", "ABC");
}

RX_TEST_CASE(testFailure)
{
    struct Fixture *pData;

    pData = (struct Fixture *)RX_FIXTURE;

    RX_CHECK_INT_EQUAL(pData->zero, 1);
}

static const struct RxTestCase cases[]
    = {{"basics", testBasics}, {"failure", testFailure}};

static const struct RxTestSuite suites[]
    = {{"example", sizeof cases / sizeof cases[0], cases, setUp, tearDown}};

int
main(int argc, const char **ppArgv)
{
    rxRun(sizeof suites / sizeof suites[0], suites, argc, ppArgv);
    return 0;
}

Which results in the output:

[PASSED] "example" / "basics" (0.009188 ms)
[FAILED] "example" / "failure" (0.010063 ms)
./rexo/tests/example.c:57: nonfatal test failure: ‘pData->zero’ is expected to be equal to ‘1’
0 == 1