article.txt (14807B)
1 == doctest - the lightest C++ unit testing framework 2 3 doctest is a fully open source light and feature-rich C++11 single-header testing framework for unit tests and TDD. 4 5 Web Site: https://github.com/doctest/doctest 6 Version tested: 2.0.0 7 System requirements: C++11 or newer 8 License & Pricing: MIT, free 9 Support: through the GitHub project page 10 11 == Introduction 12 13 A complete example with a self-registering test that compiles to an executable looks like this: 14 15 #define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN 16 #include "doctest.h" 17 18 int fact(int n) { return n <= 1 ? n : fact(n - 1) * n; } 19 20 TEST_CASE("testing the factorial function") { 21 CHECK(fact(0) == 1); // will fail 22 CHECK(fact(1) == 1); 23 CHECK(fact(2) == 2); 24 CHECK(fact(10) == 3628800); 25 } 26 27 And the output from that program is the following: 28 29 [doctest] doctest version is "1.1.3" 30 [doctest] run with "--help" for options 31 ======================================================== 32 main.cpp(6) 33 testing the factorial function 34 35 main.cpp(7) FAILED! 36 CHECK( fact(0) == 1 ) 37 with expansion: 38 CHECK( 0 == 1 ) 39 40 ======================================================== 41 [doctest] test cases: 1 | 0 passed | 1 failed | 42 [doctest] assertions: 4 | 3 passed | 1 failed | 43 44 Note how a standard C++ operator for equality comparison is used - doctest has one core assertion macro (it also has macros for less than, equals, greater than...) - yet the full expression is decomposed and the left and right values are logged. This is done with expression templates and C++ trickery. Also the test case is automatically registered - you don't need to manually insert it to a list. 45 46 Doctest is modeled after Catch [1] which is currently the most popular alternative for testing in C++ (along with googletest [5]) - check out the differences in the FAQ [7]. Currently a few things which Catch has are missing but doctest aims to eventually become a superset of Catch. 47 48 == Motivation behind the framework - how is it different 49 50 doctest is inspired by the unittest {} functionality of the D programming language and Python's docstrings - tests can be considered a form of documentation and should be able to reside near the production code which they test (for example in the same source file a class is implemented). 51 52 A few reasons you might want to do that: 53 54 - Testing internals that are not exposed through the public API and headers of a module becomes easier. 55 - Lower barrier for writing tests - you don't have to: 56 1. make a separate source file 57 2. include a bunch of stuff in it 58 3. add it to the build system 59 4. add it to source control 60 You can just write the tests for a class or a piece of functionality at the bottom of its source file - or even header file! 61 - Faster iteration times - TDD becomes a lot easier. 62 - Tests in the production code stay in sync and can be thought of as active documentation or up-to-date comments - showing how an API is used. 63 64 The framework can still be used like any other even if the idea of writing tests in the production code doesn't appeal to you - but this is the biggest power of the framework - and nothing else comes close to being so practical in achieving this - details below. 65 66 There are many other features [8] and a lot more are planned in the roadmap [9]. 67 68 This isn't possible (or at least practical) with any other testing framework for C++ - Catch [1], Boost.Test [2], UnitTest++ [3], cpputest [4], googletest [5] and many others [6]. 69 70 What makes doctest different is that it is ultra light on compile times (by orders of magnitude - further details are in the "Compile time benchmarks" section) and is unintrusive. 71 72 The key differences between it and the others are: 73 74 - Ultra light - below 10ms of compile time overhead for including the header in a source file (compared to 250-460 ms for Catch) - see the "Compile time benchmarks" section 75 - The fastest possible assertion macros - 50 000 asserts can compile for under 30 seconds (even under 10 sec) 76 - Offers a way to remove everything testing-related from the binary with the DOCTEST_CONFIG_DISABLE identifier 77 - Doesn't pollute the global namespace (everything is in the doctest namespace) and doesn't drag any headers with it 78 - Doesn't produce any warnings even on the most aggressive warning levels for MSVC / GCC / Clang 79 * -Weverything for Clang 80 * /W4 for MSVC 81 * -Wall -Wextra -pedantic and over 35 other flags not included in these! 82 - Very portable and well tested C++11 - per commit tested on CI with over 300 different builds with different compilers and configurations (gcc 4.7-8.0 / clang 3.5-6.0 / MSVC 2013-2017, debug / release, x86/x64, linux / windows / osx, valgrind, sanitizers...) 83 - Just one header and no external dependencies apart from the C / C++ standard library (which are used only in the test runner) 84 85 So if doctest is included in 1000 source files (globally in a big project) the overall build slowdown will be only ~10 seconds. If Catch is used - this would mean 350+ seconds just for including the header everywhere. 86 87 If you have 50 000 asserts spread across your project (which is quite a lot) you should expect to see roughly 60-100 seconds of increased build time if using the normal expression-decomposing asserts or 10-40 seconds if you have used the fast form [11] of the asserts. 88 89 These numbers pale in comparison to the build times of a 1000 source file project. Further details are in the "Compile time benchmarks" section. 90 91 You also won't see any warnings or unnecessarily imported symbols from doctest - nor will you see a valgrind or a sanitizer error caused by the framework - it is truly transparent. 92 93 == The main() entry point 94 95 As we saw in the example above - a main() entry point for the program can be provided by the framework. If however you are writing the tests in your production code you probably already have a main() function. The following code example shows how doctest is used from a user main(): 96 97 #define DOCTEST_CONFIG_IMPLEMENT 98 #include "doctest.h" 99 int main(int argc, char** argv) { 100 doctest::Context ctx; 101 // !!! THIS IS JUST AN EXAMPLE SHOWING HOW DEFAULTS/OVERRIDES ARE SET !!! 102 ctx.setOption("abort-after", 5); // default - stop after 5 failed asserts 103 ctx.applyCommandLine(argc, argv); // apply command line - argc / argv 104 ctx.setOption("no-breaks", true); // override - don't break in the debugger 105 int res = ctx.run(); // run test cases unless with --no-run 106 if(ctx.shouldExit()) // query flags (and --exit) rely on this 107 return res; // propagate the result of the tests 108 // your code goes here 109 return res; // + your_program_res 110 } 111 112 With this setup the following 3 scenarios are possible: 113 - running only the tests (with the --exit option) 114 - running only the user code (with the --no-run option) 115 - running both the tests and the user code 116 117 This must be possible if you are going to write the tests directly in the production code. 118 119 Also this example shows how defaults and overrides can be set for command line options. 120 121 Note that the DOCTEST_CONFIG_IMPLEMENT or DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN identifiers should be defined before including the framework header - but only in one source file - where the test runner will get implemented. Everywhere else just include the header and write some tests. This is a common practice for single-header libraries that need a part of them to be compiled in one source file (in this case the test runner). 122 123 == Removing everything testing-related from the binary 124 125 You might want to remove the tests from your production code when building the release build that will be shipped to customers. The way this is done using doctest is by defining the DOCTEST_CONFIG_DISABLE preprocessor identifier in your whole project. 126 127 The effect that identifier has on the TEST_CASE macro for example is the following - it gets turned into an anonymous template that never gets instantiated: 128 129 #define TEST_CASE(name) \ 130 template <typename T> \ 131 static inline void ANONYMOUS(ANON_FUNC_)() 132 133 This means that all test cases are trimmed out of the resulting binary - even in Debug mode! The linker doesn't ever see the anonymous test case functions because they are never instantiated. 134 135 The ANONYMOUS() macro is used to get unique identifiers each time it's called - it uses the __COUNTER__ preprocessor macro which returns an integer with 1 greater than the last time each time it gets used. For example: 136 137 int ANONYMOUS(ANON_VAR_); // int ANON_VAR_5; 138 int ANONYMOUS(ANON_VAR_); // int ANON_VAR_6; 139 140 == Subcases - the easiest way to share setup / teardown code between test cases 141 142 Suppose you want to open a file in a few test cases and read from it. If you don't want to copy / paste the same setup code a few times you might use the Subcases mechanism of doctest. 143 144 TEST_CASE("testing file stuff") { 145 printf("opening the file\n"); 146 std::ifstream is ("test.txt", std::ifstream::binary); 147 148 SUBCASE("seeking in file") { 149 printf("seeking\n"); 150 // is.seekg() 151 } 152 SUBCASE("reading from file") { 153 printf("reading\n"); 154 // is.read() 155 } 156 printf("closing... (by the destructor)\n"); 157 } 158 159 The following text will be printed: 160 161 opening the file 162 seeking 163 closing... (by the destructor) 164 opening the file 165 reading 166 closing... (by the destructor) 167 168 As you can see the test case was entered twice - and each time a different subcase was entered. Subcases can also be infinitely nested. The execution model resembles a DFS traversal - each time starting from the start of the test case and traversing the "tree" until a leaf node is reached (one that hasn't been traversed yet) - then the test case is exited by popping the stack of entered nested subcases. 169 170 == Examples of how to embed tests in production code 171 172 If shipping libraries with tests - it is a good idea to add a tag in your test case names (like this: TEST_CASE("[the_lib] testing foo")) so the user can easily filter them out with --test-case-exclude=*[the_lib]* if he wishes to. 173 174 - If you are shipping a header-only library there are mainly 2 options: 175 176 1. You could surround your tests with an ifdef to check if doctest is included before your headers - like this: 177 178 // fact.h 179 #pragma once 180 181 inline int fact(int n) { return n <= 1 ? n : fact(n - 1) * n; } 182 183 #ifdef DOCTEST_LIBRARY_INCLUDED 184 TEST_CASE("[fact] testing the factorial function") { 185 CHECK(fact(0) == 1); // will fail 186 CHECK(fact(1) == 1); 187 CHECK(fact(2) == 2); 188 CHECK(fact(10) == 3628800); 189 } 190 #endif // DOCTEST_LIBRARY_INCLUDED 191 192 2. You could use a preprocessor identifier (like FACT_WITH_TESTS) to conditionally use the tests - like this: 193 194 // fact.h 195 #pragma once 196 197 inline int fact(int n) { return n <= 1 ? n : fact(n - 1) * n; } 198 199 #ifdef FACT_WITH_TESTS 200 201 #ifndef DOCTEST_LIBRARY_INCLUDED 202 #include "doctest.h" 203 #endif // DOCTEST_LIBRARY_INCLUDED 204 205 TEST_CASE("[fact] testing the factorial function") { 206 CHECK(fact(0) == 1); // will fail 207 CHECK(fact(1) == 1); 208 CHECK(fact(2) == 2); 209 CHECK(fact(10) == 3628800); 210 } 211 #endif // FACT_WITH_TESTS 212 213 In both of these cases the user of the header-only library will have to implement the test runner of the framework somewhere in his executable/shared object. 214 215 - If you are developing an end product and not a library for developers - then you can just mix code and tests and implement the test runner like described in the section "The main() entry point". 216 217 - If you are developing a library which is not header-only - you could again write tests in your headers like shown above, and you could also make use of the DOCTEST_CONFIG_DISABLE identifier to optionally remove the tests from the source files when shipping it - or figure out a custom scheme like the use of a preprocessor identifier to optionally ship the tests - MY_LIB_WITH_TESTS. 218 219 == Compile time benchmarks 220 221 So there are 3 types of compile time benchmarks that are relevant for doctest: 222 - cost of including the header 223 - cost of assertion macros 224 - how much the build times drop when all tests are removed with the DOCTEST_CONFIG_DISABLE identifier 225 226 In summary: 227 - Including the doctest header costs under 10ms compared to 250-460 ms of Catch - so doctest is 25-50 times lighter 228 - 50 000 asserts compile for roughly 60 seconds which is around 25% faster than Catch 229 - 50 000 asserts can compile for as low as 30 seconds (or even 10) if alternative assert macros [11] are used (for power users) 230 - 50 000 asserts spread in 500 test cases just vanish when disabled with DOCTEST_CONFIG_DISABLE - all of it takes less than 2 seconds! 231 232 The lightness of the header was achieved by forward declaring everything and not including anything in the main part of the header. There are includes in the test runner implementation part of the header but that resides in only one translation unit - where the library gets implemented (by defining the DOCTEST_CONFIG_IMPLEMENT preprocessor identifier before including it). 233 234 Regarding the cost of asserts - note that this is for trivial asserts comparing 2 integers - if you need to construct more complex objects and have more setup code for your test cases then there will be an additional amount of time spent compiling - this depends very much on what is being tested. A user of doctest provides a real world example of this in his article [12]. 235 236 In the benchmarks page [10] of the project documentation you can see the setup and more details for the benchmarks. 237 238 == Conclusion 239 240 The doctest framework is really easy to get started with and is fully transparent and unintrusive - including it and writing tests will be unnoticeable both in terms of compile times and integration (warnings, build system, etc). Using it will speed up your development process as much as possible - no other framework is so easy to use! 241 242 Note that Catch 2 is on it's way (not public yet) and when it is released there will be a new set of benchmarks. 243 244 The development of doctest is supported with donations. 245 246 [1] https://github.com/catchorg/Catch2 247 [2] http://www.boost.org/doc/libs/1_60_0/libs/test/doc/html/index.html 248 [3] https://github.com/unittest-cpp/unittest-cpp 249 [4] https://github.com/cpputest/cpputest 250 [5] https://github.com/google/googletest 251 [6] https://en.wikipedia.org/wiki/List_of_unit_testing_frameworks#C.2B.2B 252 [7] https://github.com/doctest/doctest/blob/master/doc/markdown/faq.md#how-is-doctest-different-from-catch 253 [8] https://github.com/doctest/doctest/blob/master/doc/markdown/features.md 254 [9] https://github.com/doctest/doctest/issues/600 255 [10] https://github.com/doctest/doctest/blob/master/doc/markdown/benchmarks.md 256 [11] https://github.com/doctest/doctest/blob/master/doc/markdown/assertions.md#fast-asserts 257 [12] http://baptiste-wicht.com/posts/2016/09/blazing-fast-unit-test-compilation-with-doctest-11.html