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
- 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 abool
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 abool
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 (seetd_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.
EVENT
: Anenum td_event
value.TEST
: A pointer to astruct td_test_context
instance for the currently running context.
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.