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.
#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
# Debian / Ubuntu
sudo apt install libboost-test-dev
# Fedora / RHEL
sudo dnf install boost-devel
# macOS
brew install boostvcpkg
vcpkg install boost-testIn CMake, find and link the component explicitly:
find_package(Boost REQUIRED COMPONENTS unit_test_framework)
target_link_libraries(my_tests PRIVATE Boost::unit_test_framework)Conan
# conanfile.txt
[requires]
boost/1.86.0
[options]
boost/*:without_test=Falseconan install . --build=missing -pr:b=defaultHeader-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:
// 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.hppAll 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.
#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() // TokenizerBOOST_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.
| Macro | Severity | Stops test? | Notes |
|---|---|---|---|
BOOST_TEST(expr) | check | No | Universal — works for any comparable expression |
BOOST_CHECK(expr) | check | No | Boolean expression |
BOOST_REQUIRE(expr) | require | Yes | Fatal check |
BOOST_WARN(expr) | warning | No | Records but doesn't count as failure |
BOOST_CHECK_EQUAL(a, b) | check | No | a == b, prints both on failure |
BOOST_CHECK_NE(a, b) | check | No | a != b |
BOOST_CHECK_LT(a, b) | check | No | a < b |
BOOST_CHECK_LE(a, b) | check | No | a <= b |
BOOST_CHECK_GT(a, b) | check | No | a > b |
BOOST_CHECK_GE(a, b) | check | No | a >= b |
BOOST_CHECK_CLOSE(a, b, tol) | check | No | |a-b| / b < tol% |
BOOST_CHECK_CLOSE_FRACTION(a, b, tol) | check | No | Tolerance as fraction, not percent |
BOOST_CHECK_SMALL(a, tol) | check | No | |a| < tol — for values near zero |
BOOST_CHECK_THROW(expr, exc) | check | No | expr must throw exc |
BOOST_REQUIRE_THROW(expr, exc) | require | Yes | Fatal version |
BOOST_CHECK_NO_THROW(expr) | check | No | expr must not throw |
BOOST_CHECK_EXCEPTION(expr, exc, pred) | check | No | Throw + predicate on the exception |
BOOST_ERROR(msg) | check | No | Unconditional non-fatal failure |
BOOST_FAIL(msg) | require | Yes | Unconditional 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:
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
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:
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.
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:
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.
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.
#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
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
// 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
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
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
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:
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
// 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:
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.
# 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=shortExit codes
| Code | Meaning |
|---|---|
| 0 | All enabled tests passed |
| 201 | At least one test failed |
| 200 | Exception escaped a test case |
| 1 | System 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
./my_tests \
--report_format=junit \
--report_sink=test-results/results.xml \
--log_level=messageThe XML schema is compatible with Jenkins, GitLab CI, and GitHub Actions test reporting.
GitHub Actions workflow
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_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:
#define BOOST_TEST_MODULE MyLibrary
#include <boost/test/unit_test.hpp>Every other test file just includes the header:
// 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:
cmake -B build -DCMAKE_BUILD_TYPE=Debug
cmake --build build
ctest --test-dir build --output-on-failureBoost.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.
| Feature | Boost.Test | GoogleTest | Catch2 | doctest |
|---|---|---|---|---|
| Syntax style | xUnit macros | xUnit macros | Natural language | xUnit macros |
| Header-only mode | Yes | No | No (v3) | Yes |
| BDD support | No | No | Yes (SCENARIO) | No |
| Mocking | None built-in | gMock built-in | Via trompeloeil | None built-in |
| Parameterized tests | BOOST_DATA_TEST_CASE | TYPED_TEST, INSTANTIATE | GENERATE | No |
| Floating-point assertions | BOOST_CHECK_CLOSE | EXPECT_NEAR | WithinAbs | DOCTEST_CHECK |
| Decorators / metadata | Yes (labels, timeout, deps) | Via TEST_F + tags | Yes (tags) | Limited |
| JUnit XML output | Built-in | Via listener | Built-in | Built-in |
| Test filtering | --run_test=Suite/Case | --gtest_filter | tag expressions | --test-case |
| Compile-time cost | High (header-only) | Low (compiled) | Low (compiled) | Very low |
| External dependency | Requires Boost | Self-contained | Self-contained | Self-contained |
| Matchers | BOOST_TEST partial | EXPECT_THAT + Matchers | REQUIRE_THAT + Matchers | Limited |
| CMake integration | find_package(Boost) | FetchContent / find | FetchContent / find | FetchContent / 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::zipwithout 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/THENthat reads as documentation. - You value the
SECTIONmodel 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.