martinevald.net

Testdrive (GitHub)

An expressive single-header unit testing framework for C (as specified by ISO/IEC 9899:1999 or later).

Introduction

When writing any but the simplest unit tests, a lot of code typically deals with setting up preconditions. There are several approaches to this problem, the most common being employing some sort of test fixtures. These can aid when writing many different tests that rely on a complex but similar state, but the traditional approach where fixtures are data objects deals poorly with testing sequences, where the state is mutating and evolving between the test cases.

The approach taken in Testdrive is inspired by Catch2. In it, test fixtures can contain any number of nested test sections, which can extend and alter the state as needed. A section opens a new lexical scope within its parent, and will be executed in order after execution of their parent scope finishes.

Usage

Put testdrive.h somewhere in your include path. Include it where you want to write your tests.

Example

#include "testdrive.h"

// Tentative file to test.
#include "config.h" 

FIXTURE(read_configuration, "Reading configuration")
    struct conf_type* config;
    conf_prepare_mock();
    REQUIRE(conf_is_mocking());

    SECTION("Reading configuration content succeeds")
        conf_mock_read_success();
        config = conf_read("conf_file");
        REQUIRE(config);

        SECTION("Accessing existing configuration field")
            const char* field = conf_read_field(config, "foo");
            REQUIRE(strcmp(field, "bar") == 0);
        END_SECTION

        SECTION("Accessing unknown configuration field")
            const char* field = conf_read_field(config, "bar");
            REQUIRE(!field);
        END_SECTION
    END_SECTION

    SECTION("Reading configuration content fails")
        conf_mock_read_failure();
        config = conf_read("conf_file");
        REQUIRE(!config);
    END_SECTION
END_FIXTURE

int main(int argc, char** argv) {
    return RUN_TEST(read_configuration);
}

Known Limitations

Output

No output is generated by the actual tests. The tests generate a set of events that get passed on to a registered listener, which is responsible both for collecting information about the results of the tests, and for presenting output to the user.

There is currently one, default, listener included in Testdrive, which is a simple console reporter.

Additionally, running a test fixture will generate a return code of true if all assertions made succeeded, and false if any assertion (regardless of what section it was in) failed.

Main Macros

The following macros can also be prefixed by TD_, and are only available in prefixed versions if TD_ONLY_PREFIXED_MACROS is defined prior to including testdrive.h.

FIXTURE(NAME, DESCRIPTION)

Creates a new test fixture. Only works at the top level of a compilation unit.

END_FIXTURE

Marks the end of the current fixture.

SECTION(DESCRIPTION)

Creates a new section within the current fixture. Has to occur between a FIXTURE and an END_FIXTURE expansion. Technically, the section creates an if clause, and the surrounding scope will be evaluated once for each additional section defined in it, only entering one section per iteration.

END_SECTION

Marks the end of the current section.

EXTERN_FIXTURE(NAME)

Declares a test fixture that is defined in another compilation unit. Used for running test fixtures that aren’t defined locally.

REQUIRE(CONDITION)

Asserts that CONDITION is true. Does nothing apart from generate an event if true, otherwise generates an event and stops the pass over the current context, advancing to evaluate any unevaluated subsections before returning to the previous context. All code after the failing assertion within the current context will be left unevaluated, including any sections.

REQUIRE_FAIL(CONDITION)

Like REQUIRE, except will always stop the current pass. CONDITION evaluating to false results in a success report. Useful for example when a known defect is causing an assertion to fail, but fixing it is not within the scope of the current work for one reason or another, and simply inverting the test condition would cause further failed assertions down the line.

RUN_TEST(NAME)

Runs the specified test fixture.

Auxiliary Macros

These macros are mainly useful for someone who wishes to extend or alter the behaviour of Testdrive.

TD_DEFAULT_LISTENER

Macro that specifies the default listener function used by RUN_TEST(). Can be set prior to including testdrive.h, for example from the command line when building. Will be automatically set to td_console_listener, a static function defined in the header file, if undefined.

TD_SET_LISTENER(LISTENER_FUNC)

Programmatically sets a new listener function.

TD_MAX_SECTIONS

Macro that specifies the maximum number of (sub)sections within a context. Will be automatically set to 128 if undefined.

TD_MAX_NESTING

Macro that specifies the maximum nesting level in a fixture. Will be automatically set to 32 if undefined.

TD_MAX_ASSERTS

Macro that specifies the maximum number of assertions in a context. Will be automatically set to 256 if undefined.

TD_TEST_INFO(NAME)

Translates a Testdrive symbolic identifier into a C symbol referencing the base struct td_test_context instance for a test fixture.

TD_TEST_FUNCTION(NAME)

Translates a Testdrive symbolic identifier into a C symbol referencing the actual function containing the test logic for a test fixture.

TD_EVENT(EVENT, TEST)

Sends an event to the current listener. Only happens the first time a context is being iterated over.

Datatypes

enum td_event

enum td_event {
    TD_TEST_START,
    TD_SECTION_PRE,
    TD_SECTION_SKIP,
    TD_SECTION_START,
    TD_SECTION_END,
    TD_ASSERT_PRE,
    TD_ASSERT_FAILURE,
    TD_ASSERT_SUCCESS,
    TD_TEST_END
};

struct td_test_context

struct td_test_context {
    struct td_test_context* parent;
    const char* name;
    const char* description;
    size_t section_idx;
    size_t assert_count;
    bool assert_success[TD_MAX_ASSERTS];
    const char* assertions[TD_MAX_ASSERTS];
    struct td_test_context* sections;
};

td_listener

void(*td_listener)(
    enum td_event event,
    struct td_test_context* test,
    size_t sequence,
    const char* file,
    size_t line
)

License

Testdrive is released under the MIT License.