TCTest is a tiny unit test framework for C and C++. It consists of two files tctest.h and tctest.c. It is distributed under the MIT License. It has been developed and tested using Linux, but should work on other Posix-compliant operating systems.
Send comments to david.hovemeyer@gmail.com.
TCTest is modeled on JUnit 4.
The test program should define a TestObjs data type (typically a
typedef for a struct type.) An instance of TestObjs is the test
fixture, i.e., the objects/data being tested.
The test program should define setup and cleanup functions.
The setup function creates a new instance of TestObjs, and is run
automatically before each test. The cleanup function destroys an
instance of TestObjs, and is run automatically after each test.
Test functions take a pointer to an instance of TestObjs as a parameter.
They use the ASSERT macro to make assertions about expected behavior.
TCTest installs a signal handler for common runtime exceptions such
as segfault and floating-point exception, and considers these as test
failures.
Test functions are executed using the TEST macro. All invocations of
the TEST macro should be surrounded by calls to the TEST_INIT and
TEST_FINI macros. Important: the test program must call TEST_INIT,
TEST, and TEST_FINI from its main function.
Because C and C++ are memory-unsafe languages, the execution of test
functions can't be made truly independent from each other, and it is
possible that the behavior of a later test function could be affected
by the invocation of an earlier one. To make testing more robust, you
can execute a separate test program process for each test function.
See the description of tctest_testname_to_execute in the advanced
features section, and also the demo programs
(demo.c and demo_cplusplus.cpp.)
Support for C++ is fairly new (added November 5th, 2023.) However, it appears to work reasonably well.
The demo programs (demo.c and demo_cplusplus.cpp) show how to write a test program using TCTest.
The expected output of the C demo program (./demo) is
testPush...passed!
testPushMany...failed ASSERT !stackPush(objs->s, 11) at line 132
testSwapTopElts...segmentation fault (most recent ASSERT at line 142)
testSizeIsEven...floating point exception (most recent ASSERT at line 154)
testSegfaultBeforeAssert...segmentation fault
4 test(s) failed
The expected output of the C++ demo program (./demo_cplusplus) is
test_convert_to_lower_1...passed!
test_convert_to_lower_2...failed ASSERT "hello, world" == objs->s2 at line 56
test_convert_to_lower_3...std::exception (what='J causes exception for some reason!')
2 test(s) failed
We have a couple of tips for using a debugger (e.g., gdb) to
debug when a unit test fails.
First, make sure your main function uses the tctest_testname_to_execute
trick so that a command line argument can be provided to control which
unit test is executed. (See the demo programs.) That way, your execution can
run just the unit test you want to debug.
You can set a breakpoint on the tctest_fail() function if you want to
gain control of the program on the first failed ASSERT() or
FAIL().
There are a few "advanced" features you might find useful.
The FAIL macro can be used to immediate fail the current test.
The macro argument is a message describing the failure. Using this
macro is slightly nicer than doing something like ASSERT(0).
If the tctest_testname_to_execute variable is set to point to a C
string, only the test function with the name specified by the string
will be executed. This is useful for allowing the test program to
take a command line argument specifying the specific test to run.
demo.c is set up this way.
If the tctest_on_test_executed function pointer is set to a non-null
value, the specified function will be called by the TEST macro after
each test function has been executed. The function will be passed the name
of the test (as a C string) and a boolean value indicating whether or not
the named test passed. demo.c shows how to use this feature.
If the tctest_on_complete function pointer is set to a non-null value,
the specified function will be called by TEST_FINI after all tests
have been executed. This can be useful if the test program needs to do
some specialized reporting of test results. demo.c shows how
to use this feature.