Viktor Kirilov
C++ dev
by Viktor Kirilov
Tools of the trade
Passionate about
Inspired by the ability of compiled languages such as D / Rust / Nim
to write tests directly in the production code
Project mantra:
Tests can be considered a form of documentation and should be able to reside near the code which they test
Nothing is better than documentation with examples.
Nothing is worse than outdated examples that don't actually work.
Interface and functionality modeled mainly after Catch and Boost.Test / Google Test
Currently some big things which Catch has are missing:
but doctest is catching up - and is adding some of its own -
like test suites and templated test cases
#ifndef GUARD_FWD
#define GUARD_FWD
// fwd stuff...
#endif // GUARD_FWD
#if defined(DOCTEST_CONFIG_IMPLEMENT)
#ifndef GUARD_IMPL
#define GUARD_IMPL
#include <vector>
// test runner stuff...
#endif // GUARD_IMPL
#endif // DOCTEST_CONFIG_IMPLEMENT
#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
#include <doctest.h>
int fact(int n) { return n <= 1 ? n : fact(n - 1) * n; }
TEST_CASE("testing the factorial function") {
CHECK(fact(0) == 1); // will fail
CHECK(fact(1) == 1);
CHECK(fact(2) == 2);
CHECK(fact(10) == 3628800);
}
[doctest] doctest version is "1.2.4"
[doctest] run with "--help" for options
========================================================
main.cpp(6)
testing the factorial function
main.cpp(7) FAILED!
CHECK( fact(0) == 1 )
with expansion:
CHECK( 0 == 1 )
========================================================
[doctest] test cases: 1 | 0 passed | 1 failed |
[doctest] assertions: 4 | 3 passed | 1 failed |
In 2 words: light and unintrusive (transparent):
Unnoticeable even if included in every source file of a project
All tests are built in Debug / Release and in 32 / 64 bit modes.
A total of 300+ different configurations are built and tested.
This leads to:
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.
CHECK(a == 666);
CHECK(b != 42);
One core assertion macro
CHECK_EQUAL(a, 666);
CHECK_NOT_EQUAL(b, 42);
vs
TEST_CASE("name") {
// asserts
}
Automatic test case registration
TEST_CASE(unique_identifier, "name") {
// asserts
}
void some_function_called_from_main() {
doctest::register(unique_identifier);
}
vs
TEST_CASE("db") {
auto db = open("...");
SUBCASE("first tests") {
// asserts 1 with db
}
SUBCASE("second tests") {
// asserts 2 with db
}
close(db);
}
Subcases for shared setup/teardown
vs
TEST_CASE("db - first tests") {
auto db = open("...");
// asserts 1 with db
close(db);
}
TEST_CASE("db - second tests") {
auto db = open("...");
// asserts 2 with db
close(db);
}
for(int i = 0; i < 100; ++i) {
INFO("the value of i is " << i);
CHECK(a[i] == b[i]);
}
logging facilities with lazy stringification for performance
test.cpp(10) ERROR!
CHECK( a[i] == b[i] )
with expansion:
CHECK( 0 == 32762 )
with context:
the value of i is 75
will output the following:
translation of exceptions
int func() { throw MyType(); return 0; }
REG_TRANSLATOR(const MyType& e) {
return String("MyType: ") + toString(e);
}
TEST_CASE("foo") {
CHECK(func() == 42);
}
main.cpp(34) ERROR!
CHECK( func() == 42 )
threw exception:
MyType: contents...
will output the following:
stringification of user types
struct type { bool data; };
bool operator==(const type& lhs, const type& rhs) {
return lhs.data == rhs.data;
}
doctest::String toString(const type& in) {
return in.data ? "true" : "false";
}
TEST_CASE("stringification") {
CHECK(type{true} == type{false});
}
test.cpp(15) ERROR!
CHECK( type{true} == type{false} )
with expansion:
CHECK( true == false )
will output the following:
templated test cases
typedef doctest::Types<int, char, myType> types;
TEST_CASE_TEMPLATE("serialization", T, types) {
auto var = T{};
json state = serialize(var);
T result = deserialize(state);
CHECK(var == result);
}
will result in the creation of 3 test cases:
asserts for exceptions and floating point
void throws() { throw 5; }
TEST_CASE("stringification")
{
CHECK_THROWS(throws());
CHECK_THROWS_AS(throws(), int);
CHECK_NOTHROW(throws());
CHECK(doctest::Approx(5.f) == 5.001f);
}
decorators for test cases and test suites
bool is_slow() { return true; }
TEST_CASE("should be below 200ms"
* doctest::skip(is_slow())
* doctest::timeout(0.2))
{}
Code is simplified for simplicity
#define CONCAT_IMPL(s1, s2) s1##s2
#define CONCAT(s1, s2) CONCAT_IMPL(s1, s2)
#define ANONYMOUS(x) CONCAT(x, __COUNTER__)
int ANONYMOUS(ANON_VAR_); // int ANON_VAR_5;
int ANONYMOUS(ANON_VAR_); // int ANON_VAR_6;
__COUNTER__ yields a bigger integer each time it gets used
non-standard but present in all modern compilers
gets expanded to
TEST_CASE("math") {
// asserts
}
static void ANON_FUNC_24(); // fwd decl
static int ANON_VAR_25 = regTest( // register
ANON_FUNC_24, "main.cpp", 56, "math", ts::get());
void ANON_FUNC_24() { // the test case
// asserts
}
static to not clash during linking with other symbols
std::set<TestCase>& getTestRegistry() {
static std::set<TestCase> data; // static local
return data; // return a reference
}
int regTest(void (*f)(void) f, const char* file, int line
const char* name, const char* test_suite)
{
TestCase tc(name, f, file, line, test_suite);
getTestRegistry().insert(tc);
return 0; // to initialize the dummy int
}
The test registry of the test runner resides in a special getter to work around the static initialization order fiasco.
namespace ts { inline const char* get() { return ""; } } // default
TEST_SUITE("math") {
TEST_CASE("addition") { // calls ts::get()
// ...
}
}
namespace ts { inline const char* get() { return ""; } } // default
namespace ANON_TS_45 {
namespace ts { static const char* get() { return "math"; } }
}
namespace ANON_TS_45 {
TEST_CASE("addition") { // calls ts::get() ==> ANON_TS_45::ts::get()
// ...
}
}
after the preprocessor:
The framework and it's tests are clean from these:
-Wswitch-default -Wconversion -Wold-style-cast -Wfloat-equal -Wlogical-op -Wundef -Wredundant-decls -Wshadow -Wstrict-overflow=5 -Wwrite-strings -Wpointer-arith -Wcast-qual |
-Wmissing-include-dirs -Wcast-align -Wswitch-enum -Wnon-virtual-dtor -Wctor-dtor-privacy -Wsign-conversion -Wdisabled-optimization -Weffc++ -Wdouble-promotion -Winvalid-pch -Wmissing-declarations -Woverloaded-virtual |
-Wnoexcept -Wtrampolines -Wzero-as-null-pointer-constant -Wuseless-cast -Wshift-overflow=2 -Wnull-dereference -Wduplicated-cond -Wduplicated-branches -Wformat=2 -Walloc-zero -Walloca -Wrestrict |
---|
To get the list of enabled / disabled warnings - as seen in
http://stackoverflow.com/questions/11714827/#34971392
g++ -Wall -Wextra -Q --help=warning
Every (decent) compiler can do this.
#if defined(__clang__)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wpadded"
#endif // __clang__
// ... header stuff
#if defined(__clang__)
#pragma clang diagnostic pop
#endif // __clang__
The TEST_CASE macro produces warnings because of the anonymous dummy int:
We cannot ask the user to use pragmas to silence warnings....
What to do?
// test.cpp
#include "test.h"
int main() {}
#pragma pack(2)
struct T { char c; short s; };
// test.h
#define val(x) x
int a = val(5); // comment
int a = 10; // will get error
# 1 "test.h" 1
int a = 5;
int a = 10;
# 3 "test.cpp" 2
int main() {}
#pragma pack(2)
struct T { char c; short s; };
And after the preprocessor:
// test.cpp
#include <cmath>
#define myParallelTransform(op) \
_Pragma("omp parallel for") \
for(int n = 0; n < size; ++n) \
data[n] = op(data[n])
int main() {
float data[] = {0, 1, 2, 3, 4, 5};
int size = 6;
myParallelTransform(sin);
myParallelTransform(cos);
}
...
int main() {
float data[] = {0, 1, 2, 3, 4, 5};
int size = 6;
#pragma omp parallel for
for(int n = 0; n < size; ++n)
data[n] = sin(data[n]);
#pragma omp parallel for
for(int n = 0; n < size; ++n)
data[n] = cos(data[n]);
}
_Pragma() was standardized in C++11 but compilers support it for many years (__pragma() for MSVC)
#define TEST_CASE_IMPL(f, name) \
static void f(); \
\
_Pragma("clang diagnostic push") \
_Pragma("clang diagnostic ignored \"-Wglobal-constructors\"") \
\
static int ANONYMOUS(ANON_VAR_) = \
regTest(f, __FILE__, __LINE__, name, ts::get()); \
\
_Pragma("clang diagnostic pop") \
\
void f()
#define TEST_CASE(name) TEST_CASE_IMPL(ANONYMOUS(ANON_FUNC_), name)
Macro indirection needed so the same anon name is used.
#define TEST_CASE_IMPL(f, name) \
static void f(); \
\
static int ANONYMOUS(ANON_VAR_) __attribute__((unused)) = \
regTest(f, __FILE__, __LINE__, name, ts::get()); \
\
void f()
#define TEST_CASE(name) TEST_CASE_IMPL(ANONYMOUS(ANON_FUNC_), name)
_Pragma() in the C++ frontend of GCC (g++) isn't working in macros for quite some time (6+ years) - or does only sometimes
TEST_CASE("nested subcases") {
out("setup");
SUBCASE("") {
out("1");
SUBCASE("") {
out("1.1"); // leaf
}
}
SUBCASE("") {
out("2");
SUBCASE("") {
out("2.1"); // leaf
}
SUBCASE("") {
out("2.2"); // leaf
}
}
}
// THE OUTPUT
setup
1
1.1
setup
2
2.1
setup
2
2.2
SUBCASE("foo") {
// ...
SUBCASE("bar") {
// ...
}
SUBCASE("baz") {
// ...
}
}
if(const Subcase& ANON_2 = Subcase("foo", "a.cpp", 4)) {
// ...
if(const Subcase& ANON_3 = Subcase("bar", "a.cpp", 6)) {
// ...
}
if(const Subcase& ANON_4 = Subcase("baz", "a.cpp", 9)) {
// ...
}
}
vs
#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
#include <doctest.h>
#define DOCTEST_CONFIG_IMPLEMENT
#include <doctest.h>
int main(int argc, char** argv) {
doctest::Context context;
// default
context.setOption("abort-after", 5); // stop after 5 failed asserts
// apply argc / argv
context.applyCommandLine(argc, argv);
// override
context.setOption("no-breaks", true); // don't break in the debugger
// run queries or test cases unless with --no-run
int res = context.run();
if(context.shouldExit()) // query flags (and --exit) rely on this
return res; // propagate the result of the tests
// your program
return res; // + your_program_res
}
#define DOCTEST_CONFIG_DISABLE // the magic identifier
#include <doctest.h>
#define TEST_CASE(name) \
template <typename T> \
static inline void ANONYMOUS(ANON_FUNC_)()
So all test cases are turned into uninstantiated templates.
The linker doesn't even lift its finger.
This results in:
The DOCTEST_CONFIG_DISABLE identifier affects all macros - asserts and logging macros are turned into a no-op with ((void)0) - to require a semicolon - and subcases just vanish.
CHECK(a == b);
In C++ the << operator has higher precedence over ==
That is how the decomposer captures the left operand "a".
Also the "Owl" technique (0,0) used to silence C4127 in MSVC
Gets (sort of) expanded to:
do {
ResultBuilder rb("CHECK", "main.cpp", 76, "a == b");
try {
rb.setResult(ExpressionDecomposer() << a == b);
} catch(...) { rb.exceptionOccurred(); }
if(rb.log()) // returns true if the assert failed
BREAK_INTO_DEBUGGER();
} while((void)0, 0); // no "conditional expression is constant"
struct ExpressionDecomposer {
template <typename L>
LeftOperand<const L&> operator<<(const L& operand) {
return LeftOperand<const L&>(operand);
}
};
template <typename L>
struct LeftOperand{
L lhs;
LeftOperand(L in) : lhs(in) {}
template <typename R> Result operator==(const R& rhs) {
return Result(lhs == rhs, stringify(lhs, "==", rhs));
}
};
struct Result {
bool passed;
String decomposition;
Result(bool p, const String& d) : passed(p) , decomposition(d) {}
};
template <typename L, typename R>
String stringify(const L& lhs, const char* op, const R& rhs) {
return toString(lhs) + " " + op + " " + toString(rhs);
}
The default stringification of types is "{?}".
try {
rb.setResult(ExpressionDecomposer() << func() == 42);
} catch(...) { rb.exceptionOccurred(); }
int func() { throw MyType(); return 0; }
CHECK(func() == 42);
main.cpp(34) ERROR!
CHECK( func() == 42 )
threw exception:
MyType: contents...
struct ITranslator { // interface
virtual bool translate(String&) = 0;
};
template<typename T>
struct Translator : ITranslator {
String(*m_func)(T); // function pointer
Translator(String(*func)(T)) : m_func(func) {}
bool translate(String& res) override {
try {
throw; // rethrow
} catch(T ex) {
res = m_func(ex); // use the translator
return true;
} catch(...) {
return false; // didn't catch by T
}
}
};
void reg_in_test_runner(ITranslator* t); // fwd decl
template<typename T>
int regTranslator(String(*func)(T)) {
static Translator<T> t(func); // alive until the program ends
reg_in_test_runner(&t);
return 0;
}
// REG_TRANSLATOR gets expanded to:
static String ANON_TR_76(const MyType& e); // fwd decl
static int ANON_TR_77 = regTranslator(ANON_TR_76); // register
static String ANON_TR_76(const MyType& e) {
return String("MyType: ") + toString(e);
}
REG_TRANSLATOR(const MyType& e) {
return String("MyType: ") + toString(e);
}
String translate() {
// try translators
String res;
for(size_t i = 0; i < translators.size(); ++i)
if(translators[i]->translate(res)) // if success
return res;
// proceed with default translation
try {
throw; // rethrow
} catch(std::exception& ex) {
return ex.what();
} catch(std::string& msg) {
return msg.c_str();
} catch(const char* msg) {
return msg;
} catch(...) {
return "Unknown exception!";
}
}
void ResultBuilder::exceptionOccurred() { /* use translate() */ }
The doctest header is less than 1200 lines of code after the MSVC preprocessor (whitespace removed)
compared to 41k for Catch - 1.4 MB (Catch2 is 36k - 1.3 MB)
This is because doctest doesn't include anything in its forward declaration part.
The idea is not to bash Catch - it's an amazing project that continues to evolve and deserves its reputation.
Using Boost.Test in its single header form is A LOT slower...
namespace std // forbidden by the standard but works like a charm
{
template <class charT> struct char_traits;
template <> struct char_traits<char>;
template <class charT, class traits> class basic_ostream;
typedef basic_ostream<char, char_traits<char> > ostream;
}
This is how the doctest header doesn't need to include headers for std::nullptr_t or std::ostream.
Just including the <iosfwd> header with MSVC leads to 9k lines of code after the preprocessor - 450kb...
Boost.DI does the same - forward declares stuff from std and doesn't include anything
500 test cases with 100 asserts in each - 50k CHECK(a==b)
CHECK(a == b);
CHECK_EQ(a, b); // no expression decomposition
FAST_CHECK_EQ(a, b); // not evaluated in a try {} block
FAST_CHECK_EQ(a, b); // DOCTEST_CONFIG_SUPER_FAST_ASSERTS
CHECK(a == b);
CHECK(a == b); // CATCH_CONFIG_FAST_COMPILE
do {
Result res;
bool threw = false;
try {
res = ExpressionDecomposer() << a == b;
} catch(...) { threw = true; }
if(res || GCS()->success) {
do {
if(!GCS()->hasLoggedCurrentTestStart) {
logTestStart(GCS()->currentTest->m_name,
GCS()->currentTest->m_file,
GCS()->currentTest->m_line);
GCS()->hasLoggedCurrentTestStart = true;
}
} while(false);
logAssert(res.m_passed, res.m_decomposition.c_str(),
threw, "a == b", "CHECK", "a.cpp", 76);
}
GCS()->numAssertionsForCurrentTestcase++;
if(res) {
addFailedAssert("CHECK");
BREAK_INTO_DEBUGGER();
}
} while(doctest::always_false());
// CHECK(a == b) << THIS EXPANDS TO:
do {
ResultBuilder rb("CHECK", "a.cpp", 76, "a == b");
try {
rb.setResult(ExpressionDecomposer() << a == b);
} catch(...) { rb.exceptionOccurred(); }
if(rb.log()) BREAK_INTO_DEBUGGER();
} while((void)0, 0)
// FAST_CHECK_EQ(a, b) << THIS EXPANDS TO:
do {
int res = fast_binary_assert<equality>("FAST_CHECK_EQ", "a.cpp",
76, "a", "b", a, b);
if(res) BREAK_INTO_DEBUGGER();
} while((void)0, 0)
// FAST_CHECK_EQ(a, b) with #define DOCTEST_CONFIG_SUPER_FAST_ASSERTS
fast_binary_assert<equality>("FAST_CHECK_EQ", "a.cpp", 76, "a", "b", a, b);
extensive use of __declspec(noinline) / __attribute__((noinline))
The benchmarks were done on 2017.09.10 with versions:
50 000 asserts spread in 500 test cases compile for:
for(int i = 0; i < 10000000; ++i)
CHECK(i == i);
doctest 1.2 is more than 30 times faster than doctest 1.1
(talking only about the common case where all tests pass)
CppCon 2016: Nicholas Ormrod “The strange details of std::string at Facebook"
When developing end products (not libraries for developers):
OR ship the tests in the binary:
#define DOCTEST_CONFIG_IMPLEMENT
#include <doctest.h>
// later in main()
context.setOption("no-run", true); // don't run by default
context.applyCommandLine(argc, argv); // parse command line
add a tag in your test case names if shipping a library
// fact.h
#pragma once
inline int fact(int n) {
return n <= 1 ? n : fact(n - 1) * n;
}
#ifdef FACT_WITH_TESTS
#ifndef DOCTEST_LIBRARY_INCLUDED
#include <doctest.h>
#endif // DOCTEST_LIBRARY_INCLUDED
TEST_CASE("[fact] testing fact") {
CHECK(fact(0) == 1);
CHECK(fact(1) == 1);
}
#endif
// fact_usage.cpp
#include "fact.h"
// fact_tests.cpp
//#define DOCTEST_CONFIG_IMPLEMENT
#define FACT_WITH_TESTS
#include "fact.h"
// fact_tests.cpp
//#define DOCTEST_CONFIG_IMPLEMENT
#include <doctest/doctest.h>
#define FACT_WITH_TESTS
#include "fact.h"
--test-case-exclude=*[fact]*
or use a test suite
Many binaries (shared objects and executables) can share the same test runner - a single test case registry
#define DOCTEST_CONFIG_IMPLEMENTATION_IN_DLL
There are issues with self-registering test cases in static libraries which are common to all testing frameworks - for more information visit this link from the FAQ:
// doctest_proxy.h - use this header instead of doctest.h
#define DOCTEST_CONFIG_NO_SHORT_MACRO_NAMES // prefixed macros
#define DOCTEST_CONFIG_SUPER_FAST_ASSERTS // speed junkies
#include <doctest.h>
#define test_case DOCTEST_TEST_CASE
#define subcase DOCTEST_SUBCASE
#define test_suite DOCTEST_TEST_SUITE
#define check_throws DOCTEST_CHECK_THROWS
#define check_throws_as DOCTEST_CHECK_THROWS_AS
#define check_nothrow DOCTEST_CHECK_NOTHROW
#define check_eq DOCTEST_FAST_CHECK_EQ
#define check_ne DOCTEST_FAST_CHECK_NE
#define check_gt DOCTEST_FAST_CHECK_GT
#define check_lt DOCTEST_FAST_CHECK_LT
#define check DOCTEST_FAST_CHECK_UNARY
#define check_not DOCTEST_FAST_CHECK_UNARY_FALSE
Such results would not have been possible without starting from scratch.
A "modest" goal for doctest - make it the de-facto standard for unit testing in C++ (almost as a language feature).
A bit late to the party...
By Viktor Kirilov
Slides for the doctest presentation at CppCon 2017