doctest — Fastest C++ Testing Framework
doctest reference: single-header setup, TEST_CASE, SUBCASE, CHECK macros, BDD syntax, and integration with CMake and CI.
TL;DR
doctest is a single-header C++ testing framework with the fastest compile times of any test framework available. Drop in one header, define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN in exactly one translation unit, and you have a fully functional test binary with no other dependencies.
- Single header —
doctest.his the entire framework - Faster to compile than Catch2 or GoogleTest (often 2–4× faster)
- Inspired by Catch2: same
TEST_CASE/SUBCASEmodel, compatible BDD syntax - Tests can live inline in production
.cppfiles and be stripped from release builds via a single define
#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
#include <doctest/doctest.h>
TEST_CASE("vector basic operations") {
std::vector<int> v = {1, 2, 3};
CHECK(v.size() == 3);
CHECK(v[0] == 1);
REQUIRE(v.capacity() >= 3); // stops test on failure
}Run it: ./tests — no configuration, no XML, no external runner needed.
Installation
Option 1 — Single header download
Download doctest.h from the GitHub releases page and drop it anywhere on your include path. That is the entire installation.
wget https://github.com/doctest/doctest/releases/download/v2.4.11/doctest.hOption 2 — CMake FetchContent
include(FetchContent)
FetchContent_Declare(doctest
GIT_REPOSITORY https://github.com/doctest/doctest
GIT_TAG v2.4.11
)
FetchContent_MakeAvailable(doctest)
add_executable(unit_tests tests/main.cpp tests/test_math.cpp)
target_link_libraries(unit_tests PRIVATE doctest::doctest)The doctest::doctest target adds the correct include path. No additional link flags are needed.
Option 3 — vcpkg
vcpkg install doctestfind_package(doctest CONFIG REQUIRED)
target_link_libraries(unit_tests PRIVATE doctest::doctest)Option 4 — Conan
# conanfile.txt
[requires]
doctest/2.4.11
[generators]
CMakeDeps
CMakeToolchainconan install . --build=missingProject structure
One translation unit defines the main entry point. Every other file just includes the header.
tests/
├── main.cpp ← DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN here only
├── test_math.cpp
├── test_string.cpp
└── test_containers.cpp// tests/main.cpp
#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
#include <doctest/doctest.h>
// No other content needed — doctest generates main() automatically// tests/test_math.cpp
#include <doctest/doctest.h>
#include "math.h"
TEST_CASE("addition") {
CHECK(add(2, 3) == 5);
CHECK(add(-1, 1) == 0);
CHECK(add(0, 0) == 0);
}If you prefer to write your own main() (to run setup code before tests), use DOCTEST_CONFIG_IMPLEMENT instead and call doctest::Context manually — see Custom main below.
Assertions: CHECK vs REQUIRE vs WARN
doctest has three assertion families with different failure semantics:
| Macro | On failure |
|---|---|
CHECK(expr) | Records failure, continues executing the test case |
REQUIRE(expr) | Records failure, aborts the current test case immediately |
WARN(expr) | Records a warning only — does not count as a test failure |
Use REQUIRE for preconditions that make subsequent assertions meaningless if violated. Use CHECK for independent assertions so you see all failures at once.
TEST_CASE("assertion levels") {
std::vector<int> v = {10, 20, 30};
REQUIRE(v.size() == 3); // abort if size is wrong — rest depends on it
CHECK(v[0] == 10); // independent checks
CHECK(v[1] == 20);
CHECK(v[2] == 30);
WARN(v.capacity() == 3); // advisory — won't fail the build
}Negated forms
CHECK_FALSE(container.empty());
REQUIRE_FALSE(error_flag);
WARN_FALSE(deprecated_path_taken);Comparison macros (better failure messages)
The binary comparison macros produce cleaner output because doctest can show both operand values:
CHECK_EQ(result, 42); // result == 42
CHECK_NE(result, 0); // result != 0
CHECK_LT(a, b); // a < b
CHECK_GT(a, b); // a > b
CHECK_LE(a, b); // a <= b
CHECK_GE(a, b); // a >= b
REQUIRE_EQ(ptr, nullptr);Failure output for CHECK_EQ(result, 42) when result is 17:
CHECK_EQ( result, 42 ) is NOT correct!
values: CHECK_EQ( 17, 42 )Exception assertions
#include <doctest/doctest.h>
#include <stdexcept>
void risky(int x) {
if (x < 0) throw std::invalid_argument("negative");
if (x > 100) throw std::out_of_range("too large");
}
TEST_CASE("exception assertions") {
// Assert that any exception is thrown
CHECK_THROWS(risky(-1));
// Assert a specific exception type
CHECK_THROWS_AS(risky(-1), std::invalid_argument);
CHECK_THROWS_AS(risky(200), std::out_of_range);
// Assert the exception message
CHECK_THROWS_WITH(risky(-1), "negative");
CHECK_THROWS_WITH_AS(risky(-1), "negative", std::invalid_argument);
// Assert that no exception is thrown
CHECK_NOTHROW(risky(50));
}REQUIRE_THROWS* variants are also available and abort on failure, same as the regular REQUIRE.
SUBCASE — shared setup without fixtures
SUBCASE is doctest's answer to test fixtures. The outer TEST_CASE body runs once per subcase, so each subcase starts with a freshly constructed object.
TEST_CASE("database connection") {
Database db;
db.connect("test.db"); // runs before EVERY subcase
SUBCASE("insert a record") {
db.insert("user:1", "Alice");
CHECK(db.count() == 1);
CHECK(db.get("user:1") == "Alice");
}
SUBCASE("empty database has zero records") {
CHECK(db.count() == 0);
}
SUBCASE("insert then delete") {
db.insert("user:1", "Alice");
db.remove("user:1");
CHECK(db.count() == 0);
}
}Nested SUBCASE
Subcases can nest. The outer code runs for each leaf subcase.
TEST_CASE("stack operations") {
Stack<int> s;
SUBCASE("push one element") {
s.push(42);
CHECK(s.size() == 1);
CHECK(s.top() == 42);
SUBCASE("pop removes it") {
s.pop();
CHECK(s.empty());
}
SUBCASE("push another") {
s.push(7);
CHECK(s.size() == 2);
CHECK(s.top() == 7);
}
}
SUBCASE("pop on empty stack throws") {
CHECK_THROWS_AS(s.pop(), std::underflow_error);
}
}The execution order for the nested case above: push one element → pop removes it, then restart → push one element → push another, then restart → pop on empty stack throws.
BDD-style syntax
doctest ships BDD macros as aliases over TEST_CASE and SUBCASE. No extra include required.
SCENARIO("user authentication") {
GIVEN("a registered user") {
UserStore store;
store.addUser("alice", hash("secret"));
WHEN("correct credentials are supplied") {
auto token = store.login("alice", "secret");
THEN("login succeeds and returns a token") {
CHECK(token.valid());
CHECK(token.username() == "alice");
}
}
WHEN("wrong password is supplied") {
THEN("login throws AuthError") {
CHECK_THROWS_AS(store.login("alice", "wrong"), AuthError);
}
}
}
GIVEN("an unregistered user") {
UserStore store;
THEN("login throws AuthError") {
CHECK_THROWS_AS(store.login("nobody", "pw"), AuthError);
}
}
}BDD macros map directly to doctest internals:
| BDD macro | Equivalent |
|---|---|
SCENARIO(name) | TEST_CASE(name) |
GIVEN(name) | SUBCASE(name) |
WHEN(name) | SUBCASE(name) |
THEN(name) | SUBCASE(name) |
AND_WHEN, AND_THEN | SUBCASE |
Type-parameterized tests
Run the same test logic across multiple types using TEST_CASE_TEMPLATE.
#include <doctest/doctest.h>
#include <cstdint>
TYPE_TO_STRING(int);
TYPE_TO_STRING(long);
TYPE_TO_STRING(float);
TYPE_TO_STRING(double);
TEST_CASE_TEMPLATE("numeric zero-initialization", T, int, long, float, double) {
T value{};
CHECK_EQ(value, T{0});
}
TEST_CASE_TEMPLATE("arithmetic operations", T, int, float, double) {
T a = static_cast<T>(3);
T b = static_cast<T>(4);
CHECK(a + b == doctest::Approx(7).epsilon(0.001));
CHECK(b - a == doctest::Approx(1).epsilon(0.001));
}TYPE_TO_STRING is optional — it gives each instantiation a human-readable name in the test output instead of a mangled type name.
You can also pass a doctest::Types list to parameterize from a type alias:
using IntegralTypes = doctest::Types<int, unsigned int, long, std::int64_t>;
TEST_CASE_TEMPLATE("all integral types", T, IntegralTypes) {
T x = 1;
CHECK(x > 0);
}Floating-point comparison
Raw == on floats is almost never correct. Use doctest::Approx:
TEST_CASE("floating point") {
// Absolute epsilon
CHECK(3.14 == doctest::Approx(3.14159).epsilon(0.01));
// Relative epsilon (fraction of the expected value)
CHECK(1000.0 == doctest::Approx(1001.0).epsilon(0.002)); // within 0.2%
// Combine: scale controls the relative-epsilon multiplier (default 1e-5)
double result = computeCircleArea(1.0);
CHECK(result == doctest::Approx(3.14159265).epsilon(1e-6));
// REQUIRE version
REQUIRE(std::sqrt(2.0) == doctest::Approx(1.41421356).epsilon(1e-7));
}doctest::Approx is symmetric: a == Approx(b) and b == Approx(a) give the same result.
Test suites
Group related test cases with TEST_SUITE to enable suite-level filtering and reporting.
TEST_SUITE("math") {
TEST_CASE("addition") {
CHECK(add(2, 3) == 5);
}
TEST_CASE("subtraction") {
CHECK(sub(5, 3) == 2);
}
}
TEST_SUITE("string") {
TEST_CASE("to_upper converts all characters") {
CHECK(to_upper("hello") == "HELLO");
}
}Alternatively, use the open/close block form across multiple files:
// In math_tests.cpp
TEST_SUITE_BEGIN("math");
TEST_CASE("multiplication") { CHECK(mul(3, 4) == 12); }
TEST_CASE("division") { CHECK(div(10, 2) == 5); }
TEST_SUITE_END();Decorators
Decorators modify test behaviour and are appended in square brackets after the test name:
TEST_CASE("slow integration test" * doctest::skip(true)) {
// skipped unless --no-skip is passed
}
TEST_CASE("known broken feature" * doctest::may_fail(true)) {
CHECK(buggy_function() == 42); // failure recorded but doesn't fail binary
}
TEST_CASE("flaky network test" * doctest::timeout(5.0)) {
// fails if it takes more than 5 seconds
}
TEST_CASE("heavy test" * doctest::skip(!RUN_SLOW_TESTS)) {
// conditionally skip based on a runtime variable
}
// Combine decorators
TEST_CASE("ci-only test"
* doctest::description("only runs in CI")
* doctest::skip(!isCI())) {
// ...
}Test filtering from the command line
# Run all tests (default)
./tests
# Filter by test case name (wildcards supported)
./tests --test-case="vector*"
./tests --test-case="*insert*,*delete*" # comma = OR
# Exclude by name
./tests --test-case-exclude="slow*"
./tests -tce="slow*" # short form
# Filter by test suite name
./tests --test-suite="math"
./tests --test-suite="math,string"
# Skip tests decorated with doctest::skip
./tests --no-skip
# Randomize test order
./tests --order-by=rand
./tests --order-by=rand --rand-seed=42 # reproducible random
# Run tests by file
./tests --source-file="test_math.cpp"
# List all test cases without running
./tests --list-test-cases
./tests --list-test-suitesReporters and CI output
# Console output (default) — color, human-readable
./tests
# Show successful tests (not just failures)
./tests --success
./tests -s
# Show test durations
./tests --duration
./tests -d
# JUnit XML — compatible with Jenkins, GitLab CI, GitHub Actions
./tests --reporters=junit --out=results.xml
# XML (doctest native format)
./tests --reporters=xml --out=results.xml
# Quiet — only print failures and the final summary
./tests --reporters=quiet
# No color (useful for CI logs)
./tests --no-colorsGitHub Actions example
- name: Build tests
run: cmake --build build --target unit_tests
- name: Run tests
run: ./build/unit_tests --reporters=junit --out=test-results.xml
- name: Publish test results
uses: EnricoMi/publish-unit-test-result-action@v2
with:
files: test-results.xmlCMake integration
cmake_minimum_required(VERSION 3.14)
project(myproject)
include(FetchContent)
FetchContent_Declare(doctest
GIT_REPOSITORY https://github.com/doctest/doctest
GIT_TAG v2.4.11
)
FetchContent_MakeAvailable(doctest)
add_library(mylib src/math.cpp src/string_utils.cpp)
add_executable(unit_tests
tests/main.cpp
tests/test_math.cpp
tests/test_string.cpp
)
target_link_libraries(unit_tests PRIVATE doctest::doctest mylib)
# CTest integration — auto-discovers all TEST_CASE entries
include(CTest)
include(${doctest_SOURCE_DIR}/scripts/cmake/doctest.cmake)
doctest_discover_tests(unit_tests)Run via CTest:
cmake -B build -DCMAKE_BUILD_TYPE=Debug
cmake --build build
ctest --test-dir build --output-on-failureInline tests in production code
One of doctest's unique features is that tests can live in the same .cpp files as the code they test. In release builds, define DOCTEST_CONFIG_DISABLE to strip all test code at compile time with zero runtime overhead.
// src/math.cpp
#include "math.h"
#include <doctest/doctest.h>
int add(int a, int b) {
return a + b;
}
double circleArea(double r) {
return 3.14159265358979 * r * r;
}
// Tests live right here — stripped in release builds
TEST_CASE("add") {
CHECK(add(0, 0) == 0);
CHECK(add(1, 2) == 3);
CHECK(add(-5, 5) == 0);
}
TEST_CASE("circleArea") {
CHECK(circleArea(1.0) == doctest::Approx(3.14159).epsilon(0.0001));
CHECK(circleArea(0.0) == doctest::Approx(0.0));
}# Debug/test build — tests are compiled in
add_executable(unit_tests tests/main.cpp src/math.cpp)
target_link_libraries(unit_tests PRIVATE doctest::doctest)
# Release build — tests stripped out completely
target_compile_definitions(myapp PRIVATE DOCTEST_CONFIG_DISABLE)Custom main
If you need to run setup code before tests (seed a random number generator, initialize logging, connect to a test database), use DOCTEST_CONFIG_IMPLEMENT instead of DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN and write main() yourself:
// tests/main.cpp
#define DOCTEST_CONFIG_IMPLEMENT
#include <doctest/doctest.h>
#include "test_db.h"
int main(int argc, char** argv) {
// Custom setup before tests run
TestDB::initialize("postgres://localhost/test");
Logger::setLevel(Logger::SILENT);
doctest::Context ctx;
ctx.applyCommandLine(argc, argv);
ctx.setOption("no-breaks", true); // don't break into debugger on failure
int result = ctx.run(); // run all tests
// Custom teardown
TestDB::shutdown();
if (ctx.shouldExit()) // --exit flag or --list-* flags
return result;
return result;
}Logging inside tests
Use MESSAGE and CAPTURE to emit contextual information on failure:
TEST_CASE("data processing") {
std::vector<int> data = loadTestData("fixture.bin");
for (int i = 0; i < static_cast<int>(data.size()); ++i) {
CAPTURE(i); // prints: i := 7 (on failure)
CAPTURE(data[i]); // prints: data[i] := -999 (on failure)
INFO("processing index " << i); // printed only on failure
CHECK(data[i] >= 0);
}
}MESSAGE always prints. INFO and CAPTURE print only when the nearest assertion fails.
Comparison: doctest vs Catch2 vs GoogleTest
| Feature | doctest | Catch2 | GoogleTest |
|---|---|---|---|
| Compile speed | ★★★★★ | ★★★ | ★★★ |
| Single header | Yes | Yes (v2), module (v3) | No |
| SUBCASE / SECTION | SUBCASE | SECTION | No (use fixtures) |
| BDD syntax | Yes | Yes | No |
| Parameterized tests | TEST_CASE_TEMPLATE | GENERATE | TestWithParam |
| Inline production tests | Yes (strip with define) | Discouraged | No |
| Mocking built-in | No | No | gMock |
| Custom matchers | Limited | Rich | Rich |
| JUnit XML output | Yes | Yes | Yes |
| CMake test discovery | doctest_discover_tests | catch_discover_tests | gtest_discover_tests |
| Approx float compare | doctest::Approx | Catch::Approx | EXPECT_NEAR |
Choose doctest when: compile time is a constraint, you want tests inline with production code, or you are migrating from Catch2 and want a lighter alternative.
Choose Catch2 when: you need the richer GENERATE-based parameterization, or your team is already familiar with it.
Choose GoogleTest when: you need gMock for mocking, you are in a Google-adjacent ecosystem, or your CI infrastructure already integrates with gtest output.
Quick reference
// Test declaration
TEST_CASE("name") { }
TEST_CASE("name" * doctest::skip()) { }
TEST_SUITE("suite") { TEST_CASE("name") { } }
// Assertions
CHECK(expr); REQUIRE(expr); WARN(expr);
CHECK_FALSE(x); REQUIRE_FALSE(x);
CHECK_EQ(a, b); CHECK_NE(a, b);
CHECK_LT(a, b); CHECK_GT(a, b);
CHECK_LE(a, b); CHECK_GE(a, b);
// Exceptions
CHECK_THROWS(expr);
CHECK_THROWS_AS(expr, ExcType);
CHECK_THROWS_WITH(expr, "message");
CHECK_NOTHROW(expr);
// Approx
CHECK(x == doctest::Approx(y).epsilon(1e-5));
// SUBCASE
TEST_CASE("t") {
setup();
SUBCASE("a") { ... }
SUBCASE("b") { ... }
}
// BDD
SCENARIO("s") { GIVEN("g") { WHEN("w") { THEN("t") { } } } }
// Type parameterized
TEST_CASE_TEMPLATE("t", T, int, float, double) { T x{}; CHECK(x == T{0}); }
// Logging
CAPTURE(var); INFO("msg " << val); MESSAGE("always printed");See also
- Catch2 — BDD testing framework with richer matchers and
GENERATEparameterization - GoogleTest — xUnit-style framework with gMock for mocking
- sanitizers — run doctest binaries under ASan/UBSan for deeper coverage
- gcov / lcov — measure which lines your doctest suite exercises
- CMake — build system integration via
FetchContentandCTest