Skip to content

An expressive single-header unit testing framework for C.

License

Notifications You must be signed in to change notification settings

radiant64/testdrive

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

26 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Testdrive

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

  • Sections must be declared and defined within a fixture definition. There is no support for breaking functionality out into separate functions.
  • Any heap allocations made inside a fixture or a section, containing a child section, will inavoidably leak, even if freed at the end of their scope. This is an inherent effect of the setjmp()/longjmp() logic that allows a clean-slate recursive descent into nested sections sharing state.
    • For this reason, Testdrive should not be used in any program that does not have a finite and well-known lifetime. It is intended to be used in a program that runs through a set of unit tests, and then exits.
    • If running instrumentation tools like Valgrind on a program that uses Testdrive, this inherent memory leakage should be taken into account.

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.

  • NAME: Identifier for the test fixture.
  • DESCRIPTION: String describing the fixture.

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.

  • DESCRIPTION: String describing the section.

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.

  • NAME: Identifier for the test fixture.

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.

  • CONDITION: An expression which is or can be implicitly cast to a bool value.

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.

  • CONDITION: An expression which is or can be implicitly cast to a bool value.

RUN_TEST(NAME)

Runs the specified test fixture.

  • NAME: Identifier for the test fixture to run.

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.

  • LISTENER_FUNC: Function pointer to a listener function (see td_listener).

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.

  • NAME: Identifier for the 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.

  • NAME: Identifier for the 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
)

Credits

All code was written by Martin Evald (https://github.com/radiant64).

License

Testdrive is released under the MIT License.