- Notifications
You must be signed in to change notification settings - Fork96
Simple header-only C/C++ unit testing facility.
License
mity/acutest
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
Home:https://github.com/mity/acutest
Acutest is C/C++ unit testing facility aiming to be as simple as possible, notto stand in the developer's way and to minimize any external dependencies.
To achieve that, the complete implementation resides in a single C header file,and its core depends only on few standard C library functions.
Main features:
- Unit tests in C or C++ are supported.
- No need to install/setup/configure any testing framework. Acutest is justa single header file,
acutest.h
. - The header provides the program entry point (function
main()
). - Minimal dependencies: Core features only depend on few standard C headers,optional features may use more if available on the particular system.
- Trivial interface for writing unit tests: Few preprocessor macros describedfurther below.
- Rudimentary support forTest Anything Protocol(use
--tap
option). - Rudimentary support for xUnit-compatible XML output (use
--xml-output=FILE
).
C++ specific features:
- Acutest catches any C++ exception thrown from any unit test function. Whenthat happens, the given test is considered to fail.
- If the exception is derived from
std::exception
,what()
is written outin the error message.
Unix/Posix specific features:
- By default, every unit test is executed as a child process.
- By default, if the output is directed to a terminal, the output is colorized.
- If the system offers Posix timer (
clock_gettime()
), user can measure testexecution times with--time=real
(same as--time
) and--time=cpu
.
Linux specific features:
- If a debugger is detected, the default execution of tests as child processesis suppressed in order to make the debugging easier.
Windows specific features:
- By default, every unit test is executed as a child process.
- If a debugger is detected, the default execution of tests as child processesis suppressed in order to make the debugging easier.
- By default, if the output is directed to a terminal, the output is colorized.
- Acutest installs a SEH filter to print out uncaught SEH exceptions.
- User can measure test execution times with
--time
.
macOS specific features:
- If a debugger is detected, the default execution of tests as child processesis suppressed in order to make the debugging easier.
Any C/C++ module implementing one or more unit tests and includingacutest.h
,can be built as a standalone program. We call the resulted binary as a "testsuite" for purposes of this document. The suite is then executed to run thetests, as specified with its command line options.
We say any unit test succeeds if and only if:
- all condition checks (as described below) called throughout its executionpass;
- the test does not throw any exception (C++ only); and
- (on Windows or Unix) the unit test subprocess is not interrupted/terminated(e.g. by a signal on Unix or SEH on Windows).
To use Acutest, simply include the header fileacutest.h
on the beginning ofthe C/C++ source file implementing one or more unit tests. Note the headerprovides implementation of themain()
function.
#include"acutest.h"
Every test is supposed to be implemented as a function with the followingprototype:
voidtest_example(void);
The tests can use some preprocessor macros to validate the test conditions.They can be used multiple times, and if any of those conditions fails, theparticular test is considered to fail.
TEST_CHECK
is the most commonly used testing macro which simply tests aboolean condition and fails if the condition evaluates to false (or zero).
For example:
voidtest_example(void){void*mem;inta,b;mem=malloc(10);TEST_CHECK(mem!=NULL);mem=realloc(mem,20);TEST_CHECK(mem!=NULL);}
Note that the tests should be completely independent on each other. Wheneverthe test suite is invoked, the user may run any number of tests in the suite,in any order. Furthermore by default, on platforms where supported, each unittest is executed as a standalone (sub)process.
Finally, the test suite source file has to list the unit tests, using themacroTEST_LIST
. The list specifies name of each test (it has to be unique)and pointer to a function implementing the test. I recommend names which areeasy to use on command line: especially avoid space and other specialcharacters which might require escaping in shell; also avoid dash (-
) as afirst character of the name, as it could then be interpreted as a command lineoption and not as a test name.
TEST_LIST= { {"example",test_example }, ... { NULL,NULL }/* zeroed record marking the end of the list */};
Note the test list has to be ended with zeroed record.
For a basic test suites this is more or less all you need to know. HoweverAcutest provides some more macros which can be useful in some specificsituations. We cover them in the following sub-sections.
There is a macroTEST_ASSERT
which is very similar toTEST_CHECK
but, if itfails, it aborts execution of the current unit test instantly.
For example:
voidtest_example(void){void*mem;inta,b;mem=malloc(10);TEST_ASSERT(mem!=NULL);mem=realloc(mem,20);TEST_ASSERT(mem!=NULL);}
The abortion in the case of failure is performed either by callingabort()
(if the test is executed as a child process) or vialongjmp()
(if it is not).
Therefore it should be used only if you understand the costs connected withsuch a brutal abortion of the test. Depending on what your unit test does,it may include unflushed file descriptors, memory leaks, C++ objects destructedwithout their destructors being called and more.
In general,TEST_CHECK
should be preferred overTEST_ASSERT
, unless youknow exactly what you do and why you choseTEST_ASSERT
in some particularsituation.
For C++, there is an additional macroTEST_EXCEPTION
for verifying the givencode (typically just a function or a method call) throws the expected type ofexception.
The check fails if the function does not throw any exception or if it throwsanything incompatible.
For example:
voidtest_example(void){TEST_EXCEPTION(CallSomeFunction(), std::exception);}
If a condition check fails, it is often useful to provide some additionalinformation about the situation so the problem is easier to debug. Acutestprovides the macrosTEST_MSG
andTEST_DUMP
for this purpose.
The former one outputs anyprintf
-like message, the other one outputs ahexadecimal dump of a provided memory block.
So for example:
voidtest_example(void){charproduced[100];charexpected[]="Hello World!";SomeSprintfLikeFunction(produced,"Hello %s!","world");TEST_CHECK(strcmp(produced,expected)==0);TEST_MSG("Expected: %s",expected);TEST_MSG("Produced: %s",produced);/* Or, if the function could provide some binary stuff, we might rather * use TEST_DUMP instead in order to output a hexadecimal dump of the data. */TEST_DUMP("Expected:",expected,strlen(expected));TEST_DUMP("Produced:",produced,strlen(produced));}
Note that both macros output anything only when the most recently checkingmacro has failed. In other words, these two are equivalent:
if(!TEST_CHECK(some_condition!=0))TEST_MSG("some message");
TEST_CHECK(some_condition!=0);TEST_MSG("some message");
(Note thatTEST_MSG
requires the compiler with variadic macros support.)
Sometimes, it is useful to design your testing function as a loop over dataproviding a collection of test vectors and their respective expected outputs.For example imagine our unit test is supposed to verify some kind of a hashingfunction implementation and we've got a collection of test vectors for it inthe hash specification.
In such cases, it is very useful to get some name associated with every testvector and output the name in the output log so that if any check fails, it iseasy to identify the guilty test vector. However, the loop body may executedozens of checking macros and so it may be impractical to add such name tocustomize every check message in the loop.
To solve this, Acutest provides the macroTEST_CASE
. The macro specifiesa string serving as the test vector name. When used, Acutest makes sure thatin the output log the provided name precedes any message from subsequentcondition checks.
For example, lets assume we are testingSomeFunction()
which is supposed,for a given byte array of some size, return another array of bytes in a newlymalloc
-ed buffer. Then we can do something like this:
structTestVector {constchar*name;constuint8_t*input;size_tinput_size;constuint8_t*expected_output;size_texpected_output_size;};structTestVectortest_vectors[]= {/* some data */};voidtest_example(void){inti;constuint8_t*output;size_toutput_size;for(i=0;i<sizeof(test_vectors) /sizeof(test_vectors[0]);i++) {structTestVector*vec=&test_vectors[i];/* Output the name of the tested test vector. */TEST_CASE(vec.name);/* Now, we can check the function produces what it should for the * current test vector. If any of the following checking macros * produces any output (either because the check fails, or because * high `--verbose` level is used), Acutest also outputs the currently * tested vector's name. */output=SomeFunction(vec->input,vec->input_size,&output_size);if(TEST_CHECK(output!=NULL)) {TEST_CHECK(output_size==vec->expected_output_size);TEST_CHECK(memcmp(output,vec->expected_output,output_size)==0);free(output); } }}
The specified name applies to all checks executed after the use ofTEST_CASE
- until the unit test ends; or
- until
TEST_CASE
is used again to specify another name; or - until the name is explicitly reset by using
TEST_CASE
with theNULL
as its argument.
Many of the macros mentioned in the earlier sections have a counterpart whichallows to output a custom messages instead of some default ones.
All of these have the same name as the aforementioned macros, just with theunderscore suffix added. With the suffix, they then expectprintf
-like stringformat and corresponding additional arguments.
So, for example, instead of the simple checking macros
TEST_CHECK(a == b);TEST_ASSERT(x < y);TEST_EXCEPTION(SomeFunction(), std::exception);
we can use their respective counterparts with a custom messages:
TEST_CHECK_(a == b,"%d is equal to %d", a, b);TEST_ASSERT_(x < y,"%d is lower than %d", x, y);TEST_EXCEPTION_(SomeFunction(), std::exception, "SomeFunction() throws std::exception");
You should use some neutral wording for them because, with the command lineoption--verbose
, the messages are logged out even if the respective checkpasses successfully.
(If you need to output some diagnostic information just in the case the checkfails, use the macroTEST_MSG
. That's exactly its purpose.)
Similarly, instead of
TEST_CASE("name");
we can use richer
TEST_CASE_("iteration #%d",42);
However note all of these can only be used if your compiler supports variadicpreprocessor macros. Variadic macros became a standard part of the C languagewith C99.
When we are done with implementing the tests, we can simply compile it asa simple C/C++ program. For example, assumingcc
is your C compiler:
$ cc test_example.c -o test_example
When the test suite is compiled, the resulted testing binary can be used to runthe tests.
Exit code of the test suite is 0 if all the executed unit tests pass, 1 if anyof them fails, or any other number if an internal error occurs.
By default (without any command line options), it runs all implemented unittests. It can also run only subset of the unit tests as specified on thecommand line:
$ ./test_example# Runs all tests in the suite$ ./test_example test1 test2# Runs only tests specified$ ./test_example --exclude test3# Runs all tests but those specified
Note that a single command line argument can select a whole group of test unitsbecause Acutest implements several levels of unit test selection (the 1st onematching at least one test unit is used):
Exact match: When the argument matches exactly the whole name of a unittest then just the given test is selected.
Word match: When the argument does not match any complete test name, butit does match whole word in one or more test names, then all such tests areselected.
The following characters are recognized as word delimiters: space
,tabulator
\t
, dash-
, underscore_
, slash/
, dot.
, comma,
,colon:
, semicolon;
.Substring match: If even the word match failed to select any test, thenall tests with a name which contains the argument as its substring areselected.
By adopting an appropriate test naming strategy, this allows user to run (orto skip if--exclude
is used) easily whole family of related tests with asingle command line argument.
For example consider test suitetest_example
which implements testsfoo-1
,foo-2
,foomatic
,bar-1
andbar-10
:
$ ./test_example bar-1# Runs only the test 'bar-1' (exact match)$ ./test_example foo# Runs 'foo-1' and 'foo-2' (word match)$ ./test_example oo# Runs 'foo-1', 'foo-2' and 'foomatic' (substring match)$ ./test_example 1# Runs 'foo-1' and 'bar-1' (word match)
You may use--list
or-l
to just list all unit tests implemented by thegiven test suite:
$ ./test_example --list
To see description for all the supported command line options, run the binarywith the option--help
:
$ ./test_example --help
Q: Wasn't this project known as "CUTest"?
A: Yes. It has been renamed as the original name was found to betoo much overloaded.
Q: Do I need to distribute fileREADME.md
and/orLICENSE.md
?
A: No. The headeracutest.h
includes URL to our repo, copyright note andthe MIT license terms inside of it. As long as you leave those intact, we arecompletely fine if you only add the header into your project. After all,the simple use and all-in-one-header nature of it is our primary aim.
Acutest is covered with MIT license, see the fileLICENSE.md
or beginning ofacutest.h
for its full text.
The project resides on github:
You can find the latest version of Acutest there, contribute with enhancementsor report bugs.