compile-time-unit-testing


Tests that fail the build, not the board

A single-header C++20 library that runs your unit tests while the code compiles. Write an ordinary constexpr bool test_xxx() with the expect_* assertions, then static_assert(test_xxx()). A broken invariant becomes a compiler error at build time, with the actual mismatched values in the diagnostic — caught before there is anything to link, flash, or run separately.

What you get

Six things you can count on, none of which ask you to change how you build.

The build catches it, not a test run later

static_assert(test_xxx())

A broken invariant stops the compile. There is nothing to link, flash, or remember to run afterwards — if the target builds, the checks passed.

The error is already the diagnosis

expect_eq / expect_ne / expect_lt

When an assertion fails, the compiler shows the actual values that did not match, right in the error. You read the discrepancy straight from the build output instead of adding print statements to a generic assertion failure to work out what happened.

Nothing ships to the target

zero runtime · zero binary

The tests run entirely at compile time, so none of them end up in the image. No test scaffolding consumes flash or RAM on a constrained board, and there is no runtime cost to a check that finished before the binary existed.

No framework to adopt

one header · no runner

One header, no test runner, no discovery step, no separate CI job. Write a constexpr function, static_assert it, and it rides the build you already have.

It fails loudly when it should — proven

34 expected-failure tests

34 deliberately-broken compile tests regression-test the library's own failure path — checking not just that a bad invariant fails the build, but that the expected values actually appear in the diagnostic text. The tool you trust to catch mistakes is held to that same standard.

Compares what you actually need

value · C array · std::array · std::span

The assertions cover single values and whole arrays alike — C arrays, std::array, and std::span — so you compare the thing you meant to compare instead of fighting the tool into shape.

How little there is to it

Every check is a plain consteval assertion evaluated during compilation. Write a constexpr function that returns whether the invariants hold, assert the ones you care about, and static_assert the function — that is the whole integration. The assertion family is what you would expect: expect_true, expect_false, expect_eq, expect_ne, expect_lt, expect_le, expect_gt, and expect_ge, plus an epsilon-tolerant expect_near for floating-point (GCC 11+ / Clang 18+).

Works on the compilers you actually target

Header-only, MIT licensed, nothing to link, C++20 the only requirement. Tested against GCC 10–15 and Clang 13–21, plus ESP32 (Xtensa), STM32 (Cortex-M4), and RISC-V (rv32imac) bare-metal cross-compilers, and it works under -fno-exceptions — so it fits the constrained builds embedded and safety-critical work actually ships.

Applied to a real crash

A real reentrancy crash in ESPHome (issue #18434), the actual decision function extracted and tested this way, and the before-and-after compiler output proving it: the same test fails to compile against the unfixed code and passes against the fix. Not a toy example — a real, public, verifiable case study.

Install it

The repository is the maintained source of truth for install instructions — CMake's FetchContent, or vendor the single header directly. Both are documented there.

compile-time-unit-testing on GitHub →