I like single-header libraries. They are easy to manage. Usually they are flexible. Sometimes they are very lightweight. Rarely they have dependencies. I even encountered some single-header projects that have been independent from the STL. Well, the latter may go too far, however, my general preference stands: Single-header libraries are awesome.
What is not so awesome is the landscape of C++ testing frameworks. Alright, doing testing in C++ (and not using makefile voodoo with scripting languages) is not trivial. The preprocessor has to be our dear friend and we need to work out some annoying compiler differences. We also need some tricks to do proper macro expansion, automatic integration and some more code generation. But why do I need to learn a whole new language for that, install a couple of dependencies or modify my build process? Can't that be simplified?
Until some weeks ago I always just used a primitive solution I've build for that job. It worked, but it had to be adjusted to the specific project. It was a very lightweight, very simple and rudimentary solution. But then I searched for a better, universal and reliable solution. Needless to say that I found one: Catch!
Catch is a single-header library, that may turn any C++ code into a runnable test application. On demand we can use the integrated runner, define our own main
entry point or customize the predefined one. Also the structure of the unit tests is simple and elegant.
All we need is to reference the header in a way that the compiler can find it:
#include "catch.hpp"
If we want to include the predefined runner, then we should also add the following define-statement (prior to including the header):
#define CATCH_CONFIG_MAIN
#include "catch.hpp"
I tend to use the predefined runner. It gives me command line options out-of-the-box. Additionally I don't have to care about other things, like running the right tests or printing suitable output. The default runner already comes with three different output modes. This also supports IDEs like Eclipse.
Personally, I don't include the CATCH_CONFIG_MAIN
definition in a test-file, but rather in an (otherwise empty) source file, usually called runner.cpp. Otherwise it would be inconsistent and problematic to have a single test file with a main
entry point, while the others don't provide such a special function.
What is missing is a discussion of an actual test. An example of a simple test would be:
TEST_CASE( "Summing input", "[sum]" ) {
const auto size = 100;
std::vector<int> v(size);
for (int i = 0; i < size; ++i) {
v[i] = i + 1;
}
SECTION("small gauss formula should be correct") {
const auto result = sum(v.begin(), v.end());
REQUIRE(result == 5050);
}
}
Here we use three defined macros from the Catch header. We use the TEST_CASE
macro to create (and register) a function / class for a test / grouped collection of tests. Within this group we then use a SECTION
to define single tests. Each section is invoked independently. Hence modifications on, e.g., v
, in one section won't be available in another section. Additionally we can specify more detailed test descriptions.
Finally we make use of the REQUIRE
macro. I don't like that particular choice very much, however, the expansion / explanations provided by Catch make working with this macro a pleasure and much more enjoyable than with my previous ASSERT_EQUALS
et. al. attempts. A single expanded macro to rule them all - nice.
There are way more possibilities, especially for behavior testing. The library actually includes some helpers for BDD, mainly given by the Given-When-Then sections. What I also find appealing is the fact that Catch can test itself. This gives me some confidence that everything is quite solid and robust.
Just to mention two more core features: Errors can break into debuggers provided by Visual Studio. The output can be customized by writing our own reporters. Hence we are not limited to the textual and XML reporters, which are included.