This async unit testing environment differs from the traditional C++ unit testing frameworks in the fact that it incorporates a message loop and instrumentation to register and track conditions that should occur (called 'done-s' - after the name of the function that signals that the condition has occurred), within a specified timeout, and in a given order, if necessary. It also provides a means to schedule a function call after a speficied time relative to the moment of scheduling, or, in 'ordered mode' - relative to the last such 'ordered' call. The async unit test is considered complete once all expected events have occurred or timed out, and all scheduled functions have been called, or an error occurs, by either explicitly signalling it in the test code, when an event with specified order occurs in unexpected order, or an unhandled exception occurs.
In addition to asynchnorous tests, the environment supports ordinary synchronous tests, where the test is considered complete once the code of the test returns, an error is explicitly signalled by the code, or an unhandled exception occurs.
The framework is a header-only library. To use it, it is needed to only include the public header "asyncTest.hpp", and to insert the TESTS_INIT() macro in the global scope, before the main() function.
#include <promise.hpp>
//good to have the test framework header included last,
//to minimize the chance of macro conflicts
#include <asyncTest.hpp>
TESTS_INIT();
int main()
{
// global test initialization code (if any) goes here
testGroup("group one")
{
group.beforeEach = [&](test::Test& t){ printf("before each\n"); };
group.beforeEach = [&](test::Test& t){ printf("after each\n"); };
asyncTest("test one",
{{"event 1", "order", 1}, {"event 2", "timeout", 4000, "order", 2}})
{
loop.jitterPct = 40; //set the default schedCall() delay fuziness. By default it's 50%
schedCall([&]()
{
test.done("event 1");
schedCall([&]()
{
test.done("event 2");
});
});
});
asyncTest("test two", {"foo", {"bar", "timeout", 2000}})
{
promise::Promise<int> pms;
pms.then([&](int a)
{
//if a is not 34, calls test.error() and throws a test::BailoutException. Otherwise, calls test.done("foo")
doneOrError(a == 34, "foo");
})
.then([&]()
{
test.done("bar");
})
.fail([&](promise::Error& err)
{
test.error("promise should not fail");
});
loop.schedCall([&]()
{
pms.resolve(34);
}, 100);
});
syncTest("test three")
{
int a = 2;
check(a == 2); // if a is not 2, calls test.error() and throws test::BailoutException
}).disable(); //disables the test
});
// global cleanup code (if any) goes here
return test::gNumFailed; //return the total error count to the calling process. Useful for automation
}
Tests are grouped in 'test groups', each group having the option to define a function that can be executed before each and/or after each of the tests in the group.
The framework does not take control over the main() function, so it is defined by the user. The user is free to insert any
initialization/cleanup code before/after each test group. For example, global initialization code can simply be put at the
beginning of the main() function, before the definition of the first test group. However, code inside a group is not run
in sequence with the tests - tests are executed after the body of the group completes execution.
A test group is defined by the testGroup(name) { <group body> });
macro, with the name of the test group as argument.
Mind the bracket and semicolon after the closing brace. The body of the test group is executed in sequence after
the last test of the previous group has finished execution (if there is one), and any user code before the addGroup()
call.
The body has a local variable group
defined, that references the current group object. That object has the
following facilities:
group.beforeEach = <void(test::Test&) function>
If this property is set, then the specified function will be executed before each test, passing to it thetest
object that represents that test. For more info about thetest
object see the 'Local system variables' section.group.afterEach = <void(test::Test&) function>
If this property is set, then the specified function will be executed after each test, passing to it thetest
object representing that test. The function is guaranteed to be executed even if the test completed with error or exception. All exceptions that may occur inafterEach
are caught and silently ignored.
The group body can contain any code, but its purpose is to configure the test group and register tests in that group,
so normally it just contains group configuration code and a sequence of asyncTest() and syncTest() calls, which define
and add tests to the group, but do not execute them yet. The group configuration code usually does operations on the group
object. After the group body execution completes, all registered tests are executed in the sequence in which they were
registered. Finally, the main() function can return the total number of failed tests, communicating that info to the
calling process.
Async tests are defined and registered inside a group body by:
asyncTest (name [,<list of done-s>])
{
<test body>
});
Mind the closing bracket and semicolon at the end.
The name can be any string. The list of 'done' items is enclosed in braces, and each item description is in the form:
{tag [, optname1, val1 [, optname2, val2 ]]}
or, if no options are needed, it can be just a string for the tag, with no enclosing braces.
The tag
is the unique identifier of the 'done' item, which is used (in the test.done(tag)
call) to specify that
condition has occurred. What follows are optional configuration parameters for that item. They are specified as a string
option name followed by an integer value, then next option name, followed by an option value etc. Currenty there are
only two config parameters:
- 'timeout'
Specifies the time to wait for that condition (since the start of the test). If the condition does not occur within that period, the test fails with a message identifying the condition that timed out. If this option is not specified, a default timeout of 2000ms is used. - 'order'
The condition should occur in the specified order, relative to other such 'ordered' conditions (i.e. ones that have the 'order' parameter). In other words, all conditions with that config option specified must occur in the specified order relative to each other. If this option is not specified, then no order checking is done on that condition.
Synchronous tests are added by:
syncTest(name)
{
<test body>
});
Mind the closing bracket and semicolon at the end.
Any synchronous or asynchronous test can be disabled by appending .disable()
after the closing bracket of the test body
definition, see the example.
A test body has two local variables defined:
loop
(Only async tests)
The event loop inside which the asynchronous test runs (instance oftest::EventLoop
). This object has the following methods:loop.addDone({tag [,option1, val1 [, option2, val2]]})
Dynamically adds a 'done' condition to the test. The timeout starts to run since the moment theloop.addDone()
is called.loop.done(tag)
Signals that a 'done' condition has occurred. The tag identifies the condition that was specified.loop.jitterPct
The default fuzziness percent of schedCall() delays. If not set, it is 50%. See below the description ofloop.schedCall()
loop.schedCall(func, delay [, jitterPct])
Schedules a call to the specified function after the specified period (in milliseconds), with some random variance. Ifdelay
is negative, then the delay is relative to the time of the last such call (with negative delay). This allows easy setup of function call sequences by specifying the delays between them instead of all delays relative to one single point in time. Ifdelay
is positive, it is relative to the current moment.jitterPct
is the fuzziness of the actual delay as percent of the given value, i.e. the actual value randomly varies arounddelay
with max deviation ofdelay *(jitterPct/100)
.
IfjitterPct
is not specified, the loop's default (if no default set, then 50%) will be used.
test
The object (instance of classtest::Test
) representing that test. This object has the following methods:test.error(message)
Records that an error has occurred, but does not actually abort the test. After that call, normally the test should be aborted by the user via an early return, or by throwing an exception. However, throwing an exception would cause the error report to state that an exception has occurred, which can be misleading because the exception is used only to bail out. For this purpose, you can use thetest::BailoutException
class, which will be recognized by the framework and not reported.test.done(tag)
(Only async tests)
Same asloop.done(tag)
test.cleanup = <void() function>
Registers a cleanup function that will be run after the body of the test is completed. This function is guaranteed to always execute after the test body completes, even if an error/exception occurred. This function is executed before the group'safterEach
(if such is defined). An unhandled exception inside this function results in the test being marked as failed, with an error message stating that an exception has occurred in the cleanup function.
There are a few convenience macros defined by the framework, and it's a good idea to include the public header of the framework last to avoid potential conflict of these or any other macros from the framework with code in other headers.
check(cond)
Similar toassert()
- if the condition returnsfalse
,test.error()
is called, after whichtest::BailoutException
is thrown. The error message shows the condition that failed, and the source file and line.doneOrError(cond, tag)
(Only in async tests)
Callscheck(cond)
and after thattest.done(tag)
. Therefore it can be used to resolve a 'done' condition, but only in case a condition is true, and signal error if the condition is false.
There are several macros that enable additional output, and set defaults. To be used, they should be defined before the test framework header is included:
TESTLOOP_LOG_DONES
- if defined, every resolved 'done' condition will be loggedTESTLOOP_DEBUG
- if defined, enables debug info output, related to the event loopTESTLOOP_DEFAULT_DONE_TIMEOUT
- Sets the default timeout (in milliseconds) of 'done' conditions. If not set, the default is 2000ms