TestRunner Release V1.0

August 18, 2022

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.

  1. Any testable function must be in a dynamic library (.dll, .so, .dylib)
  2. A testable function must be named according to specific rules
  3. 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.


Profile picture

Written by Fredrik Kling. I live and work in Switzerland. Follow me Twitter