Organising tests
This page describes how the tests are organised and executed: the runner, the top suite, the test cases (which may be nested or repeated for several values), and the test suites.
Test runner
The test framework is implemented as a runner object, which must be instantiated; it can then be initialised, and given a top suite.
Instantiation
The runner object must be instantiated by the application:
mt::runner tr;
For simple tests, the runner can be a local object in main(). When suites are
added from other files, with add_suite(), the runner is usually a global
object, so that those files can refer to it. The runner should outlive the
static suites that refer to it.
Initialisation
After instantiation, the runner is usually initialised with the options parsed from the main arguments:
tr.initialise (argc, argv);
The initialisation is optional; without it, the runner uses the default
options (the TAP reporter, the normal verbosity, no output file, and no
filter). If used, initialise() must precede add_top_suite() and
run_suites(), which otherwise initialise the runner with the default options;
a later call is reported as an error.
On platforms without a command line, such as embedded applications without semihosting arguments, the runner can be initialised with the options set directly, starting from the defaults, instead of the main arguments:
mt::runner_options options{};
options.selected_reporter = mt::reporter_kind::human;
options.verbosity_level = mt::verbosity::verbose;
tr.initialise (options);
The members of runner_options correspond to the command-line options:
selected_reporter to --reporter, verbosity_level to --silent, --quiet,
and --verbose, output_file_path to --output-file, colour to --colour
(or --use-colour), filter_patterns to --filter, list_tests to
--list-tests (or --list), and help to --help (or -h).
The application can also construct the options from the main arguments itself, and adjust them before initialising the runner:
mt::runner_options options{ argc, argv };
options.selected_reporter = mt::reporter_kind::human;
tr.initialise (options);
The options own their strings (the command line, the output file path, and the filter patterns), and the runner keeps its own copy for the whole session, so the options and the strings they were built from need not outlive the call.
The options are taken by value, so the calls above copy them. When the options are not used after the call, they can be moved instead, which avoids the copy:
mt::runner_options options{ argc, argv };
options.selected_reporter = mt::reporter_kind::human;
tr.initialise (std::move (options));
Top suite
The test cases written directly in main() belong to the top suite, which the
application adds explicitly; the name is optional:
auto& ts = tr.add_top_suite ("Main");
ts.test ("Check something", [] (auto& t) {
t.expect (true);
});
The test cases of the top suite run immediately, when test() is called;
the top suite is always the first suite in the report. With
tr.add_top_suite (), without a name, the top suite is named after the program
(the file name of argv[0], without the extension), or default suite when
there is no command line.
The top suite is optional; without add_top_suite(), there is no top suite,
and run_suites() runs only the suites added with add_suite() and the static
suites (see Test suites). It can be added only once, and
before run_suites(); a second call returns the same suite, and a call once
run_suites() has started is rejected, so that the result is not 0. The same
applies to a second call of initialise(). A rejected call is reported on
stderr and in the report (see Rejected calls).
Test result
To run the registered suites, end the session, and return the test result as the
process exit code, the runner provides the run_suites() member function; the
session runs only once, and later calls return the same result:
return tr.run_suites ();
The result is 0 if all checks passed, and 1 otherwise, including when a
suite was added too late (see Non-static test
suites), when initialise() or add_top_suite() was
called twice, and when there is no suite at all (no top suite, no suite added
with add_suite(), and no static suite), since a test program that runs
nothing must not pass unnoticed. If the command-line options were not valid (an
unknown option, an unknown reporter name, an unknown colour mode, or a missing
option argument), the errors are reported on stderr when the options are
parsed and written on the console by the reporter (for TAP, as Bail out!
lines, followed, for an unknown option, by a comment line suggesting --help),
no output file is created, no test is run, and the result is 2. If the output
file cannot be opened, the error is reported in the same way, no test is run,
and the result is 2. If the --filter option selects no test case, the error
the filters selected no test case is reported, and the result is 2, also when
the test cases are listed. With the --help option (or -h), the usage and the
list of the options are written on the console, no test is run, and the result
is 0. When exceptions are not enabled, a failed requirement, and a call of
tr.abort (), end the test execution before the result is returned (see
Aborting the test execution).
Rejected calls
A rejected call is always written on stderr, for a script that discards the
standard output, and the result of run_suites() is not 0. It is also reported
in the report and in the output file, so that they tell why the session failed:
as a failed check of the test case that is running, or of the suite that is
running (the top suite included), shown as the message suite 'X' added after run_suites() started, rejected; and, when no test case or suite is running, for
example after run_suites() returned, as a comment line after the summary,
since the report is complete.
Refer to the Test Runners reference page.
Test cases
Test cases are collections of checks to be executed within the same environment.
Test case objects can be created using the test() member function provided by
the test suite object:
template <typename Callable_T, typename... Args_T>
void test (const located_name& name, Callable_T&& func, Args_T&&... arguments);
The body is called with a reference to the test case. It is usually written
with auto&, as in the examples; the parameter may be declared as
mt::test_case& instead, which gives much shorter compiler diagnostics
when the body has a mistake.
A body that cannot be called with a test_case& and the additional arguments,
such as one that takes no parameter, an int, or a mt::suite&, is rejected
with a message that states what the body of a test case takes, not with the
trace of the concepts. The bodies of the suites, passed to add_suite() or to
the static_suite constructor, are checked in the same way: they take a
mt::suite& first, and they and their additional arguments must be copyable,
since the suite runs later. A generic body (with an auto& parameter) is not
checked, since checking it would instantiate its code; a mistake inside it is
reported by the compiler at its place.
The name is a string literal, a const char*, a std::string, or a
std::string_view, converted implicitly to located_name, which copies it and
also records the location of the test() call. A temporary string, such as
prefix + " overflow", is moved instead of copied, and a view need not be
null-terminated. A char array is copied up to its first null character, or up
to its end if it has none, so that a fixed-size name field that is full,
without a terminator, is read only within the array; a char pointer must
point to a null-terminated string. The copy is freed when the test case ends.
The names of the suites, given to add_suite(), add_top_suite(), and the
static_suite constructor, are located_name objects too, and are kept by the
suites.
A helper function that adds test cases on behalf of its caller can take
a const mt::located_name& parameter and pass it to test(), so that
the location reported for the test case is that of the call to the
helper:
void
check_answer (mt::suite& ts, const mt::located_name& name, int value)
{
ts.test (name, [value] (auto& t) { t.expect (mt::eq (value, 42)); });
}
The additional arguments, if any, are passed to the callable after the test
case reference. They are copied when test() is called, as they are by
runner::add_suite() and by the static_suite constructor, whose suites run
later, so a reference parameter refers to the copy. To refer to the
caller's object, pass a pointer, or std::ref (object) for a parameter of type
T&. A lambda can also capture the values instead.
Without additional arguments, the callable of a test case is not copied,
since the test case runs and ends within the call to test(); it is called in
place, so a callable that cannot be copied, such as a lambda that captures a
std::unique_ptr, is accepted, and a callable object passed as an lvalue keeps
the changes of its state between the calls.
When exceptions are enabled, an exception that escapes the body of a test case
is reported as a failed check of that test case, at the location of the
test() call, and the run continues with the next test case; the message
includes the result of what() for a std::exception:
• Parse configuration
✓ header is valid
✗ unexpected exception: invalid key FAILED (config.cpp:42)
✗ Parse configuration - FAILED (1 check passed, 1 failed)
Similarly, an exception that escapes the body of a test suite, outside its test cases (for example from a set-up step), is reported as a failed check of that suite, after the test cases already run; the rest of the suite body is skipped, and the run continues with the next suite. Since the suites do not record a source location, none is shown:
• Configuration suite
✓ Parse configuration - passed (1 check)
✗ unexpected exception in the suite body: no such file FAILED
✗ Configuration suite - FAILED (1 check passed, 1 failed, in 1 test case)
For the exception to reach the framework, the body, of a test case or of a test
suite, must not be declared noexcept; otherwise the exception calls
std::terminate(), and ends the program.
Refer to the Test Cases reference page.
Nested test cases
Test cases can be defined in a nested manner, i.e., a test case can include not only checks, but also other test cases, at any nesting level.
A nested test case is created by calling test() on the outer test case, and
runs immediately; a failed nested test case also fails the outer one:
ts.test ("Parse configuration", [] (auto& t)
{
t.expect (parse ("config.json")) << "configuration parsed";
t.test ("Defaults", [] (auto& nt)
{
nt.expect (default_port () == 80) << "default port";
});
});
A test case started on the suite, with ts.test(), from the body of a
running test case of that suite (for example via a captured suite reference),
cannot follow it in the report, since the running test case has not ended; it
runs nested in the innermost running test case, as if t.test() had
been called.
A test case can be started only on a suite whose body is running, and nested
only on the test case that runs. A call on any other suite, for example on a
static suite from main(), before the runner was initialised, or after
run_suites() returned, or on a suite other than the running one, from the
body of a test case, is rejected. The same happens to t.test() called on an
outer test case while a nested one runs. The rejected test case is not run;
the failure is reported as a failed check of the running test case, or, when
none runs, in the running suite, or on stderr when no suite runs, and
run_suites() returns 1.
The counts of the outer test case do not include the checks of its nested
test cases, which are reported on their own lines, while the counts of the
suite and of the totals include them; an outer test case with no checks of its
own and a failed nested one is therefore reported as
FAILED (0 checks passed, 0 failed, 1 test case failed).
The checks of a nested test case are made on its own parameter (nt in the
example). A check made on the outer test case (t, captured by reference)
while the nested one runs would be written inside the report of the nested test
case; it is reported instead as a failure of the running test case, with a
message that names the two test cases.
Each nesting level adds to the stack used by the test; see Stack usage.
Test cases for each value
The same test can be run for several values with test_each(), which
adds and runs a test case for each value, on the suite or, nested, on
a test case; the callable receives the test case and the value:
ts.test_each ("Buffer sizes", { 1, 16, 512 }, [] (auto& t, int size)
{
t.expect (mt::eq (make_buffer (size).size (), size));
});
The values may be a braced list, a range (a container, an array, a span, or a view), or a tuple, whose values may have different types, for a generic callable:
ts.test_each ("Serialise", std::tuple{ 42, 1.5, "text" },
[] (auto& t, const auto& value)
{
t.expect (mt::eq (deserialise (serialise (value)), value));
});
Each test case is named after the common name, followed by the value in
parentheses, written as in the messages (Buffer sizes (16),
Serialise (text)); a value that cannot be written, an application type
without an operator<<, is replaced by its position, starting at 1
(Records (#2)). The values are passed to the callable by reference, not
copied in advance as the arguments of test() are, since each test case runs
within the iteration; a parameter declared by value receives its own copy.
Test suites
Test suites are named sequences of test cases.
Static test suites
In the common use case, test suites are static instances of the static_suite
class, constructed with a reference to a runner, which runs them. Each
static suite inserts itself into a global list of suites, sorted by name,
when it is constructed, before main(). The list does not depend on the runner
object, so the suites may be constructed before the runner, in any order, and in
any source file; each runner runs only its own suites, in the order of the
list.
The main program looks like this:
#include "micro-os-plus/micro-test-plus.h"
namespace mt = micro_os_plus::micro_test_plus;
mt::runner tr;
int
main (int argc, char* argv[])
{
tr.initialise (argc, argv);
// Without add_top_suite(), there is no top suite; run_suites() runs only the
// static suites. If tests are also needed in main(), add it:
// auto& ts = tr.add_top_suite ("Top suite");
return tr.run_suites ();
}
Each test suite resides in a separate source file, for example:
#include "micro-os-plus/micro-test-plus.h"
namespace mt = micro_os_plus::micro_test_plus;
extern mt::runner tr;
static void
suite_function (mt::suite& ts)
{
ts.test ("Check various conditions", [] (auto& t)
{
t.test ("Check truth", [] (auto& nt) {
nt.expect (true);
});
});
}
static mt::static_suite static_suite_1 = { "Static suite", tr, suite_function };
The implementation uses the C++ static constructors that are performed
before the main() function is called. The order of registration is not
defined; to provide a certain degree of predictability, the framework runs
all suites when the run_suites() member function is called, after ordering
them by name.
The source files that define static suites must be linked into the program as
object files. If they are packed in a static library, the linker drops the
object files that no symbol refers to, and since nothing refers to a static
suite, the suite silently does not run. Add the files directly to the
executable, or use a CMake OBJECT library; the FAQ explains the
problem in more detail.
The names are compared byte by byte, as with strcmp(): the comparison is
case-sensitive ("Zeta" runs before "alpha"), and numbers are not
compared by value ("Suite 10" runs before "Suite 2"). To run the suites
in a given order, start their names with zero-padded numbers ("01 - Init",
"02 - Run"), and use the same case.
A static_suite may also be a local object, constructed in a function, but only
before the run_suites() of its runner has started; one constructed later would
never run, so it is rejected, the error is reported (see Rejected
calls), and the result is not 0. The runner should outlive the
static suites that refer to it.
Non-static test suites
Static test suites are just a convenience, since they are run automatically by their runner, and not including the source file with the test suite in the build automatically disables the tests.
However, they are not mandatory; suites can also be added to the runner
explicitly, with add_suite().
#include "micro-os-plus/micro-test-plus.h"
namespace mt = micro_os_plus::micro_test_plus;
extern void
suite_function (mt::suite& ts);
mt::runner tr;
int
main (int argc, char* argv[])
{
tr.initialise (argc, argv);
auto& ts = tr.add_top_suite ("Minimal");
ts.test ("Check truth", [] (auto& t) {
t.expect (true);
});
tr.add_suite ("External suite", suite_function);
return tr.run_suites ();
}
The order of the calls to add_suite() is ignored. The added suites
run when run_suites() is called, together with the static suites, sorted
by name, byte by byte (see above). Only for equal names, they run
after the static suites constructed before main(), in the order of the
calls.
For example, if "Zeta" is added first and "Alpha" second, "Alpha"
runs first. To control the order, choose the suite names accordingly,
for example with a numeric prefix, such as "01 Setup" and
"02 Processing".
The suites must be added before run_suites() is called. A suite added
while run_suites() runs, for example from the body of another suite, or after
it returned, would never run; it is rejected, the error is reported (see
Rejected calls), and the result is not 0.
The suite name can be a string literal, a char array, a const char*,
a std::string, or a std::string_view, with the same rules as the name of
a test case; it is copied, so the string need not outlive the call to
add_suite().
Several runners
A program may have several runners, each with its own static suites and suites
added with add_suite(). All the suites are kept in a single list, shared by
the runners, but each suite refers to its runner, and run_suites() runs
only the suites that refer to it; the suites of the other runners are
skipped, so the runners do not interfere:
mt::runner first_runner;
mt::runner second_runner;
static mt::static_suite a{ "A", first_runner, suite_a_function };
static mt::static_suite b{ "B", second_runner, suite_b_function };
int
main (int argc, char* argv[])
{
first_runner.initialise (argc, argv);
first_runner.add_suite ("C", suite_c_function);
second_runner.initialise (argc, argv);
int exit_code = first_runner.run_suites (); // Runs A and C.
exit_code += second_runner.run_suites (); // Runs B.
return exit_code;
}
Each runner has its own session, with its own options, report, totals, and result. If the runners write to an output file, give each one a different file, since each session overwrites it.
The list of suites is not protected by a lock; the runners must not run, and the suites must not be created or destroyed, concurrently from different threads.
Example
An even more elaborate example from a real-world project is the following test
of the double list links of another µOS++ package. Its member functions are
named initialize() and uninitialized(), with the American spelling, as in
that package; they are not µTest++ functions. The runner tr is defined in
another source file of the application.
namespace os = micro_os_plus;
namespace mt = micro_os_plus::micro_test_plus;
extern mt::runner tr;
// Define a function template to run the tests.
template <class T>
void
check_double_list_links (mt::suite& ts)
{
static T left_links;
static T links;
static T right_links;
ts.test ("Initial", [&] (auto& t) {
if constexpr (T::is_statically_allocated::value)
{
// Check if the node is cleared.
t.expect (mt::eq (links.previous (), nullptr)) << "prev is null";
t.expect (mt::eq (links.next (), nullptr)) << "next is null";
t.expect (links.uninitialized ()) << "uninitialised";
left_links.initialize ();
links.initialize ();
right_links.initialize ();
}
t.expect (!left_links.linked ()) << "left unlinked";
t.expect (!links.linked ()) << "unlinked";
t.expect (!right_links.linked ()) << "right unlinked";
});
ts.test ("Link", [&] (auto& t) {
// Link left as previous.
links.link_previous (&left_links);
// Link right as next.
links.link_next (&right_links);
// The node must now appear as linked.
t.expect (links.linked ()) << "linked";
t.expect (mt::eq (left_links.next (), &links)) << "left linked";
t.expect (mt::eq (right_links.previous (), &links)) << "right linked";
});
ts.test ("Unlink", [&] (auto& t) {
// Unlink the central node.
links.unlink ();
t.expect (!links.linked ()) << "unlinked";
// Left and right must indeed point to each other.
t.expect (mt::eq (left_links.next (), &right_links)) << "left -> right";
t.expect (mt::eq (right_links.previous (), &left_links)) << "right <- left";
});
if constexpr (!T::is_statically_allocated::value)
{
ts.test ("Allocated on stack", [] (auto& t) {
T stack_links;
t.expect (!stack_links.linked ()) << "unlinked";
});
}
}
// Instantiate the test for statically allocated lists.
static mt::static_suite ts_static_double_list_links
= {
"Static double list links", tr,
check_double_list_links<os::utils::static_double_list_links>
};
// Instantiate the same test for regular lists.
static mt::static_suite ts_double_list_links
= { "Double list links", tr,
check_double_list_links<os::utils::double_list_links> };
Refer to the Test Suites reference page.
Aborting the test execution
When exceptions are not enabled, a failed requirement cannot stop only its test
case (see Requirements), so it aborts the
whole test execution: the remaining test cases and suites do not run,
run_suites() does not return, and no totals are written. The test execution
can also be aborted explicitly, from a test case, from a suite, or from the
application, by calling tr.abort (). Before the process is terminated, the
reporter completes the report, in place of the totals:
- it displays the results of the running test case, if any; after a failed
requirement, they end with it, and the human-readable reporter marks it as
test execution aborted(the TAP reporter adds no marker, since itsBail out!line, below, reports the abort); they are preceded by the name of the test case, and by those of the outer test cases, unless already displayed, so that a failure cannot be attributed to the previous test case; - it ends the output with a line with the location of the failed requirement,
or of the
abort()call:✗ Test execution aborted at <file>:<line>for the human-readable reporter, andBail out! Requirement failed at <file>:<line>(orBail out! Test execution aborted at <file>:<line>, for an explicit abort) for the TAP reporter, which tells the TAP consumers that the run stopped early, not that the output was truncated.
Then runner::abort() flushes the standard streams, calls the abort handler, if
one was set (see below), and then ::abort(). An abort before initialise(),
or after run_suites(), when there is no report to complete, is reported on
stderr, as error: test execution aborted at <file>:<line>.
With --quiet, which otherwise displays only the failed checks and the totals,
the failed checks of the aborted test case are displayed as well, since the
totals are never reached. With --silent, nothing is displayed. If an output
file is used, the results and the final line are written to it too, whatever the
verbosity.
For example, with the human-readable reporter, the end of the output is:
✓ Check various conditions - passed (2 checks)
• Parse configuration
✓ file found
✗ sections parsed FAILED (main.cpp:42, 0 > 0) - test execution aborted
✗ Test execution aborted at main.cpp:42
and with the TAP reporter:
# Subtest: Parse configuration
ok 1 - file found
not ok 2 - sections parsed
---
condition: "0 > 0"
at:
fileName: main.cpp
lineNumber: 42
...
Bail out! Requirement failed at main.cpp:42
An explicit abort is intended for the failures after which running the other
tests is pointless, or even harmful, whether exceptions are enabled or not: for
example, when the device or the environment that all the tests use cannot be
initialised. Unlike a failed require() with exceptions, which stops only its
test case, tr.abort () always stops the whole test execution. The runner
must be reachable from the test case: a global runner, as in the examples of
this guide, can be used directly, while a local one must be captured by
reference, as in [&tr]:
ts.test ("Open the device", [] (auto& t)
{
t.expect (device.open ()) << "device opened";
if (!device.is_open ())
{
// No other test can run without the device.
tr.abort ();
}
});
With the TAP reporter, the end of the output is then:
# Subtest: Open the device
not ok 1 - device opened
---
at:
fileName: main.cpp
lineNumber: 57
...
Bail out! Test execution aborted at main.cpp:61
Unless the abort handler ends the program otherwise, the process ends with
SIGABRT, so a test harness sees a terminated process, not an exit code
returned by run_suites().
On embedded targets, where terminating the process is not meaningful, the
application can set a handler, called before ::abort(), for example to reset
the target or to signal the result to the debugger:
tr.set_abort_handler ([] (const mt::reflection::source_location& sl)
{
// Signal the failure, then reset the target; do not return.
});
If the handler returns, ::abort() is still called.
When exceptions are enabled, the handler may also throw an exception, to return
control to the application without terminating the process, for example when the
test runner is only one part of a larger program. The test cases and the suites
do not catch it, as they do with the other exceptions, so it leaves
run_suites(), or the test() call that started a test case of the top suite,
and reaches the application:
struct test_aborted {};
tr.set_abort_handler ([] (const mt::reflection::source_location&)
{ throw test_aborted{}; });
int result;
try
{
result = tr.run_suites ();
}
catch (const test_aborted&)
{
result = 3; // The report is already complete.
}
The abort is final: the report is complete, no test case runs afterwards,
and initialise() and run_suites() report an error on stderr
(run_suites() returns 1). Without exceptions, the handler must not
return.
Refer to the Test Runners reference page.