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

Boost.Test — Unit Testing Framework

Boost.Test for C++ — test suites, test cases, assertions, fixtures, parameterized tests, decorators, and CMake integration.

TL;DR

Boost.Test is the oldest production-grade C++ testing framework still actively evolving. If your project already depends on Boost, you get a full-featured test framework at zero additional cost. It supports header-only and compiled modes, rich assertions, data-driven tests, decorators, and JUnit XML output out of the box — no plugins required.

cpp
#define BOOST_TEST_MODULE MyLibrary
#include <boost/test/included/unit_test.hpp>

BOOST_AUTO_TEST_SUITE(Math)

BOOST_AUTO_TEST_CASE(AddIntegers)
{
    int result = 2 + 2;
    BOOST_REQUIRE(result > 0);       // fatal — stops test on failure
    BOOST_CHECK_EQUAL(result, 4);    // non-fatal — records and continues
}

BOOST_AUTO_TEST_CASE(DivisionByZeroThrows)
{
    BOOST_CHECK_THROW(divide(1, 0), std::domain_error);
}

BOOST_AUTO_TEST_SUITE_END()

BOOST_REQUIRE halts the current test case; BOOST_CHECK records the failure and continues. Choose based on whether subsequent assertions are meaningful if the first one fails.


Installation

Three paths exist; header-only is the fastest to start with but adds compile time at scale.

System package

bash
# Debian / Ubuntu
sudo apt install libboost-test-dev

# Fedora / RHEL
sudo dnf install boost-devel

# macOS
brew install boost

vcpkg

bash
vcpkg install boost-test

In CMake, find and link the component explicitly:

cmake
find_package(Boost REQUIRED COMPONENTS unit_test_framework)
target_link_libraries(my_tests PRIVATE Boost::unit_test_framework)

Conan

ini
# conanfile.txt
[requires]
boost/1.86.0

[options]
boost/*:without_test=False
bash
conan install . --build=missing -pr:b=default

Header-only vs compiled

Header-only (#include <boost/test/included/unit_test.hpp>) requires no linker flags and works in single-file setups. It recompiles the framework with every translation unit that includes it. For projects with many test files, link against the compiled libboost_unit_test_framework instead:

cpp
// Do NOT define BOOST_TEST_MODULE in every file.
// Define it only in one file that owns main().
#define BOOST_TEST_MODULE MyLibrary
#include <boost/test/unit_test.hpp>   // not included/unit_test.hpp

All other test files simply include <boost/test/unit_test.hpp> without the module define.


Basic Test Structure

Boost.Test organizes tests into a tree: module → suites → cases. The module is declared once per binary via BOOST_TEST_MODULE. Suites are optional but indispensable for large test files — they group related cases and can be nested arbitrarily.

cpp
#define BOOST_TEST_MODULE StringUtils
#include <boost/test/included/unit_test.hpp>

// Top-level suite
BOOST_AUTO_TEST_SUITE(Tokenizer)

BOOST_AUTO_TEST_CASE(SplitOnComma)
{
    auto parts = split("a,b,c", ',');
    BOOST_REQUIRE_EQUAL(parts.size(), 3u);
    BOOST_CHECK_EQUAL(parts[0], "a");
    BOOST_CHECK_EQUAL(parts[2], "c");
}

BOOST_AUTO_TEST_CASE(EmptyInputReturnsOneEmptyToken)
{
    auto parts = split("", ',');
    BOOST_CHECK_EQUAL(parts.size(), 1u);
}

    // Nested suite
    BOOST_AUTO_TEST_SUITE(Unicode)

    BOOST_AUTO_TEST_CASE(HandlesMultibyteCharacters)
    {
        auto parts = split("α,β,γ", ',');
        BOOST_CHECK_EQUAL(parts.size(), 3u);
    }

    BOOST_AUTO_TEST_SUITE_END()  // Unicode

BOOST_AUTO_TEST_SUITE_END()  // Tokenizer

BOOST_AUTO_TEST_SUITE_END() is a raw macro without arguments — it closes the most recently opened suite. Nesting is purely textual; indenting nested content is convention, not requirement.


Assertions

The assertion family is larger than most frameworks, giving you precise control over severity and what gets compared.

MacroSeverityStops test?Notes
BOOST_TEST(expr)checkNoUniversal — works for any comparable expression
BOOST_CHECK(expr)checkNoBoolean expression
BOOST_REQUIRE(expr)requireYesFatal check
BOOST_WARN(expr)warningNoRecords but doesn't count as failure
BOOST_CHECK_EQUAL(a, b)checkNoa == b, prints both on failure
BOOST_CHECK_NE(a, b)checkNoa != b
BOOST_CHECK_LT(a, b)checkNoa < b
BOOST_CHECK_LE(a, b)checkNoa <= b
BOOST_CHECK_GT(a, b)checkNoa > b
BOOST_CHECK_GE(a, b)checkNoa >= b
BOOST_CHECK_CLOSE(a, b, tol)checkNo|a-b| / b < tol%
BOOST_CHECK_CLOSE_FRACTION(a, b, tol)checkNoTolerance as fraction, not percent
BOOST_CHECK_SMALL(a, tol)checkNo|a| < tol — for values near zero
BOOST_CHECK_THROW(expr, exc)checkNoexpr must throw exc
BOOST_REQUIRE_THROW(expr, exc)requireYesFatal version
BOOST_CHECK_NO_THROW(expr)checkNoexpr must not throw
BOOST_CHECK_EXCEPTION(expr, exc, pred)checkNoThrow + predicate on the exception
BOOST_ERROR(msg)checkNoUnconditional non-fatal failure
BOOST_FAIL(msg)requireYesUnconditional fatal failure

The REQUIRE_ variants exist for every CHECK_ macro. Replace CHECK with REQUIRE, or replace WARN with REQUIRE to escalate severity.

BOOST_TEST — the universal assertion

BOOST_TEST is the most powerful single assertion. It decomposes expressions automatically and provides detailed failure output without the _EQUAL/_LT verbosity:

cpp
BOOST_AUTO_TEST_CASE(UniversalAssertions)
{
    std::vector<int> v{1, 2, 3};

    BOOST_TEST(v.size() == 3u);
    BOOST_TEST(v[0] < v[1]);
    BOOST_TEST(v.back() == 3);

    // Bitwise comparison — avoids operator== shortcutting
    BOOST_TEST(0b1010 == 0b1010, boost::test_tools::bitwise());

    // Custom message
    BOOST_TEST(v.size() > 0u, "vector must not be empty after construction");
}

Floating-point

cpp
BOOST_AUTO_TEST_CASE(FloatingPoint)
{
    double pi = 3.14159265358979;

    // Percentage tolerance: |a - b| / |b| * 100 < 0.001%
    BOOST_CHECK_CLOSE(pi, 3.14159, 0.001);

    // Absolute tolerance for near-zero values
    BOOST_CHECK_SMALL(pi - 3.14159265358979, 1e-12);

    // BOOST_TEST with tolerance via per_element for collections
    std::vector<double> got{1.0, 2.0, 3.0};
    std::vector<double> exp{1.0, 2.0, 3.0};
    BOOST_TEST(got == exp, boost::test_tools::per_element());
}

Exception predicates

BOOST_CHECK_THROW only verifies the type; BOOST_CHECK_EXCEPTION lets you inspect the thrown object:

cpp
BOOST_AUTO_TEST_CASE(ExceptionContent)
{
    auto pred = [](const std::invalid_argument& e) {
        return std::string(e.what()).find("negative") != std::string::npos;
    };

    BOOST_CHECK_EXCEPTION(
        compute(-1),
        std::invalid_argument,
        pred
    );
}

Test Fixtures

Fixtures eliminate repeated setup/teardown code while keeping test cases independent.

Struct-based fixtures

Boost.Test uses structs, not inheritance from a base class. Constructor runs before the test body; destructor runs after — including on assertion failures.

cpp
struct DatabaseFixture {
    DatabaseFixture()
        : conn("host=localhost dbname=test_db")
    {
        conn.execute("BEGIN");
    }

    ~DatabaseFixture()
    {
        conn.execute("ROLLBACK");
    }

    DbConnection conn;
};

BOOST_FIXTURE_TEST_CASE(InsertRecord, DatabaseFixture)
{
    int id = conn.execute_returning_id(
        "INSERT INTO users(name) VALUES('alice') RETURNING id"
    );
    BOOST_CHECK_GT(id, 0);
}

BOOST_FIXTURE_TEST_CASE(QueryRecords, DatabaseFixture)
{
    auto rows = conn.query("SELECT * FROM users WHERE id > 0");
    BOOST_CHECK_GE(rows.size(), 0u);
}

Each test case gets a fresh DatabaseFixture instance — the transaction is always rolled back, leaving the database clean regardless of test outcome.

Suite-level fixtures

Apply the same fixture to every case in a suite without repeating it on each BOOST_FIXTURE_TEST_CASE:

cpp
struct HttpServerFixture {
    HttpServerFixture()  { server.start(8080); }
    ~HttpServerFixture() { server.stop(); }
    TestHttpServer server;
};

BOOST_FIXTURE_TEST_SUITE(ApiTests, HttpServerFixture)

BOOST_AUTO_TEST_CASE(GetHealthEndpoint)
{
    auto resp = server.get("/health");
    BOOST_CHECK_EQUAL(resp.status, 200);
}

BOOST_AUTO_TEST_CASE(PostCreatesResource)
{
    auto resp = server.post("/items", R"({"name":"foo"})");
    BOOST_CHECK_EQUAL(resp.status, 201);
}

BOOST_AUTO_TEST_SUITE_END()

BOOST_AUTO_TEST_CASE inside a BOOST_FIXTURE_TEST_SUITE inherits the fixture automatically. Use BOOST_FIXTURE_TEST_CASE inside the suite if a particular case needs a different fixture.

Global fixtures

Global fixtures run once per test binary, not per test case. They're the right place for expensive one-time setup: loading test data from disk, spinning up a container, compiling a shader program.

cpp
struct GlobalEnv {
    GlobalEnv()  { TestEnvironment::init(); }
    ~GlobalEnv() { TestEnvironment::teardown(); }
};

BOOST_TEST_GLOBAL_FIXTURE(GlobalEnv);

BOOST_TEST_GLOBAL_FIXTURE registers the fixture in the master test suite. The destructor runs after all test cases have completed.


Parameterized Tests

BOOST_DATA_TEST_CASE runs the test body once per element in a dataset — no test case duplication, no manual loops.

cpp
#include <boost/test/included/unit_test.hpp>
#include <boost/test/data/test_case.hpp>
#include <boost/test/data/monomorphic.hpp>

namespace bdata = boost::unit_test::data;

Single dataset

cpp
BOOST_DATA_TEST_CASE(ParseIntegers,
    bdata::make({0, 1, -1, 42, std::numeric_limits<int>::max()}),
    value)
{
    std::string s = std::to_string(value);
    BOOST_CHECK_EQUAL(parseInt(s), value);
}

Each element becomes a separate test case in the report. value is the injected parameter name — it matches the last argument to the macro.

Range and random

cpp
// Discrete range: 0, 2, 4, 6, 8
BOOST_DATA_TEST_CASE(EvenNumbers,
    bdata::xrange(0, 10, 2),
    n)
{
    BOOST_CHECK_EQUAL(n % 2, 0);
}

// Random doubles in [0.0, 1.0), 50 samples
BOOST_DATA_TEST_CASE(RandomProbabilities,
    bdata::random(0.0, 1.0) ^ bdata::make(bdata::xrange(50)),
    p, index)
{
    BOOST_CHECK_GE(p, 0.0);
    BOOST_CHECK_LT(p, 1.0);
}

zip — paired datasets

cpp
BOOST_DATA_TEST_CASE(StringRoundTrip,
    bdata::make({"hello", "world", "foo"}) ^ bdata::make({5u, 5u, 3u}),
    str, expected_len)
{
    BOOST_CHECK_EQUAL(str.size(), expected_len);
}

^ zips datasets element-by-element. Both must have the same length or one must be infinite (like bdata::random).

Cartesian product

cpp
BOOST_DATA_TEST_CASE(MatrixOperations,
    bdata::make({2, 4, 8}) * bdata::make({1, 2, 4}),
    rows, cols)
{
    Matrix m(rows, cols);
    BOOST_CHECK_EQUAL(m.size(), static_cast<size_t>(rows * cols));
}

* computes the Cartesian product. With 3 row values and 3 column values, this generates 9 test cases.

Mixed datasets with custom types

cpp
struct Config { std::string name; int threads; bool compressed; };

std::ostream& operator<<(std::ostream& os, const Config& c)
{
    return os << c.name;
}

BOOST_DATA_TEST_CASE(ConfigVariants,
    bdata::make({
        Config{"fast",   4,  false},
        Config{"safe",   1,  true},
        Config{"turbo", 16, false},
    }),
    cfg)
{
    auto result = process(cfg.threads, cfg.compressed);
    BOOST_CHECK(result.success);
}

Boost.Test uses operator<< for the config type in failure messages and test names. Providing it makes reports readable.


Test Decorators

Decorators annotate test cases and suites with metadata that Boost.Test's runner acts on — no subclassing or registration callbacks required.

Decorators attach via the * operator immediately before the test macro:

cpp
namespace utf = boost::unit_test;

BOOST_AUTO_TEST_CASE(SlowIntegrationTest,
    * utf::timeout(30)
    * utf::label("slow")
    * utf::label("integration"))
{
    // Fails if it takes more than 30 seconds
}

Available decorators

cpp
// Disable a test without deleting it
BOOST_AUTO_TEST_CASE(BrokenOnWindows,
    * utf::disabled())
{
    BOOST_CHECK(platformSpecificFeature());
}

// Mark expected failures — test passes if exactly N assertions fail
BOOST_AUTO_TEST_CASE(KnownBug,
    * utf::expected_failures(1))
{
    BOOST_CHECK_EQUAL(brokenFunction(), 42);  // known to return 43
}

// Run this case only after another passes
BOOST_AUTO_TEST_CASE(DependentTest,
    * utf::depends_on("Initialization/SetupDatabase"))
{
    BOOST_CHECK(databaseIsReady());
}

// Description shown in listings
BOOST_AUTO_TEST_CASE(DescribedTest,
    * utf::description("Verifies that the cache invalidates on TTL expiry"))
{
    BOOST_CHECK(cacheExpired());
}

// Timeout in seconds
BOOST_AUTO_TEST_CASE(TimedTest,
    * utf::timeout(5))
{
    computeAnswer();
}

Suite-level decorators

Decorators on suites apply to all contained cases:

cpp
BOOST_AUTO_TEST_SUITE(SlowSuite,
    * utf::label("slow")
    * utf::timeout(60))

BOOST_AUTO_TEST_CASE(FirstSlowTest) { /* ... */ }
BOOST_AUTO_TEST_CASE(SecondSlowTest) { /* ... */ }

BOOST_AUTO_TEST_SUITE_END()

Running Tests and Filtering

The test binary produced by Boost.Test is a full command-line interface — no separate test runner needed.

bash
# Run all tests
./my_tests

# List the test tree without running
./my_tests --list_content

# Run a specific suite
./my_tests --run_test=Math

# Run a specific case in a suite
./my_tests --run_test=Math/AddIntegers

# Wildcard matching
./my_tests --run_test=Math/*Throws*

# Run by label (requires --run_test=@label syntax)
./my_tests --run_test=@slow
./my_tests --run_test=@integration

# Exclude a suite
./my_tests --run_test=!SlowSuite

# Log levels: error, warning, message, test_suite, unit_scope, all
./my_tests --log_level=all

# Report formats: confirm, short, detailed, no
./my_tests --report_level=detailed

# JUnit XML output
./my_tests --report_format=junit --report_sink=results.xml

# Catch-all for multiple options
./my_tests --run_test=Math --log_level=message --report_level=short

Exit codes

CodeMeaning
0All enabled tests passed
201At least one test failed
200Exception escaped a test case
1System error (bad argument, setup failure)

CTest maps non-zero exit codes to FAIL automatically — no special configuration required.


Output Formats and CI

Boost.Test's built-in JUnit output lands directly into CI artifact stores without a converter.

JUnit XML report

bash
./my_tests \
    --report_format=junit \
    --report_sink=test-results/results.xml \
    --log_level=message

The XML schema is compatible with Jenkins, GitLab CI, and GitHub Actions test reporting.

GitHub Actions workflow

yaml
name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-24.04

    steps:
      - uses: actions/checkout@v4

      - name: Install Boost
        run: sudo apt-get install -y libboost-test-dev

      - name: Configure
        run: cmake -B build -DCMAKE_BUILD_TYPE=Release

      - name: Build
        run: cmake --build build --parallel

      - name: Run tests
        run: |
          mkdir -p test-results
          ./build/my_tests \
            --report_format=junit \
            --report_sink=test-results/results.xml \
            --log_level=message
        continue-on-error: true

      - name: Publish test results
        uses: mikepenz/action-junit-report@v4
        if: always()
        with:
          report_paths: test-results/results.xml

      - name: Upload artifacts
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: test-results
          path: test-results/

continue-on-error: true on the test step ensures the upload and report steps always run even when tests fail. The JUnit report action annotates individual pull request commits with failing test names.


CMake Integration

CMake's find_package handles Boost components cleanly; each component becomes a modern imported target.

cmake
cmake_minimum_required(VERSION 3.20)
project(MyLibrary CXX)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# --- Production library ---
add_library(mylib
    src/math.cpp
    src/parser.cpp
)
target_include_directories(mylib PUBLIC include)

# --- Tests ---
enable_testing()

find_package(Boost REQUIRED COMPONENTS unit_test_framework)

add_executable(mylib_tests
    tests/main.cpp
    tests/math_test.cpp
    tests/parser_test.cpp
)

target_link_libraries(mylib_tests
    PRIVATE
        mylib
        Boost::unit_test_framework
)

# Define BOOST_TEST_DYN_LINK when linking the shared library variant.
# Not needed when linking static (the default on most systems).
target_compile_definitions(mylib_tests
    PRIVATE
        BOOST_TEST_DYN_LINK
)

add_test(
    NAME MyLibraryTests
    COMMAND mylib_tests
        --report_format=junit
        --report_sink=${CMAKE_BINARY_DIR}/test-results.xml
        --log_level=message
)

# Propagate test failure through CTest
set_tests_properties(MyLibraryTests PROPERTIES
    FAIL_REGULAR_EXPRESSION "FAILED"
)

tests/main.cpp owns the module declaration:

cpp
#define BOOST_TEST_MODULE MyLibrary
#include <boost/test/unit_test.hpp>

Every other test file just includes the header:

cpp
// tests/math_test.cpp
#include <boost/test/unit_test.hpp>
#include "math.hpp"

BOOST_AUTO_TEST_SUITE(Math)

BOOST_AUTO_TEST_CASE(Add)
{
    BOOST_CHECK_EQUAL(add(2, 3), 5);
}

BOOST_AUTO_TEST_SUITE_END()

Run via CTest:

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

Boost.Test vs Other Frameworks

The framework you choose shapes your test code as much as your production code — switching later is expensive. Pick based on what you'll actually need.

FeatureBoost.TestGoogleTestCatch2doctest
Syntax stylexUnit macrosxUnit macrosNatural languagexUnit macros
Header-only modeYesNoNo (v3)Yes
BDD supportNoNoYes (SCENARIO)No
MockingNone built-ingMock built-inVia trompeloeilNone built-in
Parameterized testsBOOST_DATA_TEST_CASETYPED_TEST, INSTANTIATEGENERATENo
Floating-point assertionsBOOST_CHECK_CLOSEEXPECT_NEARWithinAbsDOCTEST_CHECK
Decorators / metadataYes (labels, timeout, deps)Via TEST_F + tagsYes (tags)Limited
JUnit XML outputBuilt-inVia listenerBuilt-inBuilt-in
Test filtering--run_test=Suite/Case--gtest_filtertag expressions--test-case
Compile-time costHigh (header-only)Low (compiled)Low (compiled)Very low
External dependencyRequires BoostSelf-containedSelf-containedSelf-contained
MatchersBOOST_TEST partialEXPECT_THAT + MatchersREQUIRE_THAT + MatchersLimited
CMake integrationfind_package(Boost)FetchContent / findFetchContent / findFetchContent / find

Choose Boost.Test when:

  • Your project already links Boost, so there is no additional dependency to justify.
  • You need data-driven tests with Cartesian products and bdata::zip without writing a custom generator.
  • You want test metadata (labels, timeouts, dependencies) without a plugin.

Choose GoogleTest when:

  • You need gMock's mock object capabilities — nothing else matches its depth.
  • Your team knows JUnit conventions and wants minimal context switching.

Choose Catch2 when:

  • You prefer BDD-style SCENARIO/GIVEN/WHEN/THEN that reads as documentation.
  • You value the SECTION model for sharing setup without explicit fixture structs.
  • You're testing a header-only library and want zero-overhead inclusion (v3's compiled mode is small).

Choose doctest when:

  • Compile time is a hard constraint (doctest has the lowest overhead of the four).
  • You want to embed tests directly in production headers without affecting release builds.

See Also

Edit on GitHubUpdated 2026-07-14T00:00:00.000Z