TestRunner - my C/C++ unit testing harness - has been released as version 1.0, this is a write up of the features and uses.
Introduction
TestRunner (http://github.com/gnilk/testrunner) is a C/C++ unit test tool for macOs (x86, arm), Windows and Linux. It is heavily inspired by GOLANG’s unit test framework.
Since the inception in 2018 I have used this on many difference projects both private and professionally, fine tuning it as I’ve gone along. I feel it is stable enough to properly tag it as Release V1.0.
Howto
As with everything there are a few rules to how it works.
- Any testable function must be in a dynamic library (.dll, .so, .dylib)
- A testable function must be named according to specific rules
- A testable function must take one argument (interface pointer) and return an integer
That’s basically it. Generally I set up a project to compile a dylib of all code + test-functions. This is simple enough in CMake, just another shared library target.
The naming of test functions must follow a specific pattern: test_module_case Let’s say we have some code:
static bool ComputeStuff(int a, int b) {
return a+b;
}
extern "C" {
int test_compute_stuff(void *t) {
int res = ComputeStuff(1,2);
TR_ASSERT(t, res==3);
return kTR_Pass;
}
}
You compile this as a dynamic library and then execute trun on it.
trun mylib.dll
The testrunner will now load the library and look for any exported function named test_
. By default it will execute each such function and check the return value (Pass/Fail) and dump the result in an easy to read manner.
At the end of each run a summary is written out of any failed test cases.
Running the example from the repository would print (only last test shown):
localhost@cmake-build-debug % trun lib/libexshared.dylib
=== RUN _test_exit
17.07.2022 20:11:43.034 [0x16f4e3000] DEBUG - - /Users/gnilk/src/github.com/testrunner/src/exshared/exshared.cpp:87:test_exit
=== PASS: _test_exit, 0.000 sec, 0
-------------------
Duration......: 0.052 sec
Tests Executed: 14
Tests Failed..: 4
Failed:
[Tma]: _test_pure_main
[Tma]: _test_shared_a_error
[Tma]: _test_shared_b_assert, /Users/gnilk/src/github.com/testrunner/src/exshared/exshared.cpp:39, 1 == 2
[tMa]: _test_shared_b_fatal, /Users/gnilk/src/github.com/testrunner/src/exshared/exshared.cpp:32, this is a fatal error (stop all further cases for module)
Each test case is executed in a separate thread. The thread will be stopped/terminated in case an error occurs (like if you issue an ASSERT).
There is a small header file testinterface.h
you should include in the source where you keep your unit tests. This file declares some return codes and the argument struct pointer passed to any testable function.
Structuring projects
I generally structure projects like:
CMakeLists.txt
bld\
src\
unit_a.cpp
unit_b.cpp
tests\
test_unit_a.cpp
test_unit_b.cpp
I generally compile all units to a static library and then a special dynamic library with the tests (which links the static library).
This way it is easy to create a custom target with CMake that runs the testrunner over the tests.
project(myproject C CXX)
# main source
list(APPEND project_src src/unit_a.cpp)
list(APPEND project_src src/unit_b.cpp)
# unit tests
list(APPEND project_test_src src/tests/test_unit_a.cpp)
list(APPEND project_test_src src/tests/test_unit_b.cpp)
add_library(project_lib STATIC ${project_src})
add_library(project_utests SHARED ${project_test_src})
target_link_libraries(project_lib)
target_link_libraries(project_utests project_lib)
add_custom_target(
Unit_Tests ALL
DEPENDS project_utests
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
)
The above provides a good basis and structure for keeping the tests close to the source but still separated and not cluttering the main source structure.