Unit testing in C: a tutorial with AcuTest

I would like to give you a short tutorial on unit testing with AcuTest as the driving example. We go from initial examples to a more detailed understanding.
AcuTest is one of many light-weight minimalist C/C++ unit testing framework. It does not need any installation and comes in a single header file.
0. Setup on Linux
Let's begin with setting a directory for our examples:
mkdir acutest-demo
cd acutest-demo
We only need a single header file out of the AcuTest repository to get everything else going:
curl -fsSLo acutest.h https://raw.githubusercontent.com/mity/acutest/master/include/acutest.h
1. First example
We write the file demo.c as follows:
#include "acutest.h"
static int add(int a, int b)
{
return a + b;
}
void test_addition(void)
{
int sum = add(4, 5);
TEST_CHECK(sum == 9);
TEST_CHECK(add(0, 0) == 0);
}
TEST_LIST = {
{ "test-addition", test_addition },
{ NULL, NULL }
};
We compile and run the test like a normal C program:
gcc -std=c99 -Wall -Wextra -pedantic demo.c -o demo
./demo
We examine this first example. There is no main function in our own code because the entry point to the program is within the AcuTest header. Our own code only provides the tests.
Each AcuTest program contains a list of tests that are included in the final executable. These are given by tuples of 1. an identifying string and 2. a function pointer to the actual test instructions. The end of the list must be signaled by a pair { NULL, NULL } of null pointers. That list is assigned to the variable TEST_LIST (which is actually a macro). In our example, the list of tests contains only a single test:
TEST_LIST = {
{ "test-addition", test_addition },
{ NULL, NULL }
};
The function that provide will perform a few checks using the macro TEST_CHECK. It simply accepts some expression and records whether that expression evaluates to true. In practice, the condition in TEST_CHECK( condition ) will indicate whether our software produces the expected result. Each test may have multiple checks. The test is successful if it all checks have received true conditions.
2. Multiple tests and multiple checks
We extend this example:
#include "acutest.h"
static int add(int a, int b)
{
return a + b;
}
static int sub(int a, int b)
{
return a - b;
}
static int mult(int a, int b)
{
return a * b;
}
void test_addition(void)
{
int result = add(2, 3);
TEST_CHECK(result == 5);
TEST_CHECK(add(0, 0) == 0);
}
void test_subtraction(void)
{
TEST_CHECK(sub(8, 3) == 5);
TEST_CHECK(sub(4, 3) == 2); // will fail
TEST_CHECK(sub(4, 3) == 0); // will fail, too
}
void test_multiplication(void)
{
TEST_CHECK(mult(4, 5) == 20);
TEST_CHECK(mult(-1,-2) == 2);
TEST_CHECK(mult(5, 2) == mult(2, 5));
}
TEST_LIST = {
{ "test-addition", test_addition },
{ "test-subtraction", test_subtraction },
{ "test-multiplication", test_multiplication },
{ NULL, NULL }
};
We compile and execute the unit test collection. In addition, we echo its most recent output.
gcc -std=c99 -Wall -Wextra -pedantic demo.c -o demo
./demo
echo $?
We have multiple tests. The tests are run independently of each other and for each we record whether they fail or succeed. Each test may have multiple checks. A test succeeds if all checks in that test are successful, and fails otherwise. However, even if one check fails, the remaining checks in that same test are still executed.
Each test is given as a function with no arguments and no return value.
We have also emitted the return value of ./demo using the command echo $?. The number returned by the AcuTest executable is 0 if all tests have succeeded and 1 otherwise.
3. Basic command-line options for AcuTest
The AcuTest executable exemplifies many basic features of unit test frameworks. All the features of the executable are provided by the header file, we only need to write the tests and announce them to the framework.
We can run specific tests by passing them as command-line arguments:
./demo test-addition test-multiplication
We can also exclude specific tests from the execution:
./demo --exclude test-addition test-multiplication
./demo -X test-addition test-multiplication
Instead of running anything, we can instruct the AcuTest executable to simply list all available tests:
./demo -l
./demo --list
There are numerous ways to control the depth of the output. First, there are no less than three ways to suppress all output:
./demo --verbose=0
./demo -q
./demo --quiet
If we only want the test results displayed and skip over the individual checks, then we can write
./demo --verbose=1
The default version corresponds to level 2, as in:
./demo
./demo --verbose=2
Finally, we display all passed checks with summaries on the highest level of verbosity:
./demo -v
./demo --verbose=3
A short summary of the available options is found in help message of the program:
./demo --help
./demo -h
4. Generating checks within a test
The individual checks in each test are the individual invocations of TEST_CHECK during a test. This may be part of a loop and perhaps not even known at runtime.
#include "acutest.h"
static int add(int a, int b)
{
return a + b;
}
static int sub(int a, int b)
{
return a - b;
}
static int mult(int a, int b)
{
return a * b;
}
void test_addition(void)
{
int result = add(2, 3);
TEST_CHECK(result == 5);
TEST_CHECK(add(0, 0) == 0);
}
void test_subtraction(void)
{
TEST_CHECK(sub(8, 3) == 5);
TEST_CHECK(sub(4, 3) == 2); // will fail
TEST_CHECK(sub(4, 3) == 0); // will fail, too
}
void test_multiplication(void)
{
TEST_CHECK(mult(4, 5) == 20);
TEST_CHECK(mult(-1,-2) == 2);
TEST_CHECK(mult(5, 2) == mult(2, 5));
}
void test_arithmetics(void)
{
for( int a = 1; a < 5; a++ )
for( int b = 1; b < 5; b++ )
{
TEST_CHECK( ( ( a / b ) * b ) == a );
}
}
TEST_LIST = {
{ "test-addition", test_addition },
{ "test-subtraction", test_subtraction },
{ "test-multiplication", test_multiplication },
{ "more-arithemetics", test_arithmetics },
{ NULL, NULL }
};
gcc -std=c99 -Wall -Wextra -pedantic demo.c -o demo
./demo --verbose=3
echo $?
We see that the successful and failing checks in the more-arithmetics could easily go into the millions with a small change in the parameters.
Summary
We have glimpsed into the functionality of AcuTest, which is a prototypical unit test framework for C. It also demonstrates how these frameworks package unit tests into an executable environment.



