C++ Unit Testing With CMake and Catch2

...and precompiled headers!

I've used CMake for around a year or so and programmed in C++ for maybe half of that time as of right now, and recently I've finally had a chance to write unit tests for a project of mine. Previous projects were either too simple or too reliant on third party software to warrant the effort but that project really lent itself to it.

Problem Statement

I wanted the workflow for building and testing the project to be

cd build
cmake ..
make
make test

and I wanted to see the test output when running make test. This disqualified utilities like CTest since they hide the output of the test driver without proving an option to show it.

I didn't realize this until I actually ran a test for the first time, but I also want test compiles and runs to be fast. This is why I spent quite a bit of time on figuring out how to precompile the Catch2 header.

Solution

The unit test folder is split up in two groups: (1) the Catch2 main function and (2) the actual tetst.

└── tests
    ├── catch.h
    ├── CMakeLists.txt
    ├── main.cpp
    └── tests.cpp

Directory listing of the tests folder in my project.

main.cpp contains the following two lines

#define CATCH_CONFIG_MAIN
#include "catch.h"

while tests.cpp contains the TEST_CASEs.

The CMakeLists.txt in the tests/ folder contains the following code:

# This exclusively contains the catch2 main define, separated because precompiling
# headers would be a problem otherwise.
add_library(tests-main STATIC main.cpp)

# This contains the actual tests.
add_executable(tests-run tests.cpp)

target_link_libraries(tests-run tests-main)

target_compile_definitions(tests-run PRIVATE CATCH_CONFIG_FAST_COMPILE
	CATCH_CONFIG_DISABLE_MATCHERS)

# Important if you don't want the test compile to take >5s every time.
target_precompile_headers(tests-run PRIVATE catch.h)

add_custom_target(test "tests-run" "-d yes")

The comments in the first couple of lines reveal that the Catch2 main has to be its own file in order to allow headers to be precompiled. The reason for this is sneaky and (in my opinion) borders on being a bug: If CATCH_CONFIG_MAIN is defined, #include "catch.h" expands with the implementation code (which would usually reside in .cpp files), otherwise it expands without it.

Precompiling would therefore cause the same implementation code to be expanded for all instances of #include "catch.h" and not just the one time it should usually be expanded, leading to linking errors.

In the above CMake code, this problem is sidestepped by compiling Catch2's main as a library which is then linked to the actual tests. I got this idea from a blog post by Mochan Shrestha, and it works beautifully so far.

The last line looks simple, and it really is simple: it adds a custom target called target which builds and runs tests-run.

That's it. make test works as expected. Test compilation is slow for the first compile but really quick afterwards.