doctest

FORK: The fastest feature-rich C++11/14/17/20 single-header testing framework
git clone https://git.neptards.moe/neptards/doctest.git
Log | Files | Refs | README

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