Skip to content
C++
Analysis & Formatting
Updated 2026-07-14T00:00:00.000Z

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.h is the entire framework
  • Faster to compile than Catch2 or GoogleTest (often 2–4× faster)
  • Inspired by Catch2: same TEST_CASE/SUBCASE model, compatible BDD syntax
  • Tests can live inline in production .cpp files and be stripped from release builds via a single define
cpp
#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.

bash
wget https://github.com/doctest/doctest/releases/download/v2.4.11/doctest.h

Option 2 — CMake FetchContent

cmake
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

bash
vcpkg install doctest
cmake
find_package(doctest CONFIG REQUIRED)
target_link_libraries(unit_tests PRIVATE doctest::doctest)

Option 4 — Conan

ini
# conanfile.txt
[requires]
doctest/2.4.11

[generators]
CMakeDeps
CMakeToolchain
bash
conan install . --build=missing

Project structure

One translation unit defines the main entry point. Every other file just includes the header.

cpp
tests/
├── main.cpp          ← DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN here only
├── test_math.cpp
├── test_string.cpp
└── test_containers.cpp
cpp
// tests/main.cpp
#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
#include <doctest/doctest.h>
// No other content needed — doctest generates main() automatically
cpp
// 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:

MacroOn 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.

cpp
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

cpp
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:

cpp
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:

cpp
CHECK_EQ( result, 42 ) is NOT correct!
  values: CHECK_EQ( 17, 42 )

Exception assertions

cpp
#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.

cpp
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.

cpp
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.

cpp
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 macroEquivalent
SCENARIO(name)TEST_CASE(name)
GIVEN(name)SUBCASE(name)
WHEN(name)SUBCASE(name)
THEN(name)SUBCASE(name)
AND_WHEN, AND_THENSUBCASE

Type-parameterized tests

Run the same test logic across multiple types using TEST_CASE_TEMPLATE.

cpp
#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:

cpp
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:

cpp
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.

cpp
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:

cpp
// 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:

cpp
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

bash
# 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-suites

Reporters and CI output

bash
# 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-colors

GitHub Actions example

yaml
- 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.xml

CMake integration

cmake
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:

bash
cmake -B build -DCMAKE_BUILD_TYPE=Debug
cmake --build build
ctest --test-dir build --output-on-failure

Inline 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.

cpp
// 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));
}
cmake
# 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:

cpp
// 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:

cpp
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

FeaturedoctestCatch2GoogleTest
Compile speed★★★★★★★★★★★
Single headerYesYes (v2), module (v3)No
SUBCASE / SECTIONSUBCASESECTIONNo (use fixtures)
BDD syntaxYesYesNo
Parameterized testsTEST_CASE_TEMPLATEGENERATETestWithParam
Inline production testsYes (strip with define)DiscouragedNo
Mocking built-inNoNogMock
Custom matchersLimitedRichRich
JUnit XML outputYesYesYes
CMake test discoverydoctest_discover_testscatch_discover_testsgtest_discover_tests
Approx float comparedoctest::ApproxCatch::ApproxEXPECT_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

cpp
// 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 GENERATE parameterization
  • 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 FetchContent and CTest
Edit on GitHubUpdated 2026-07-14T00:00:00.000Z