Lesson 25 · Quality & debugging

Testing with CTest

Register and run tests with CTest. enable_testing, add_test, discover_tests for GTest, and the ctest command that runs everything.

Your code has tests. CMake’s job is to run them all with one command and tell you which failed. That is CTest, a test driver that runs registered tests and reports results.

The minimum

tests/CMakeLists.txt
enable_testing() add_test(NAME test_calc COMMAND test_calc)
ctest --test-dir build
Test project .../build
    Start 1: test_calc
1/1 Test #1: test_calc .................   Passed  0.01 sec
100% tests passed, 0 tests failed out of 1

Note

enable_testing(). Call it at the top level of your project, because that is where ctest looks for the test file. add_test then registers each test executable. A test passes if it exits with code 0.

The add_test signature to use

Use the NAME ... COMMAND ... form. The old one-argument form still exists, but it is less flexible. The named form supports generator expressions, target names and options:

add_test(NAME test_calc
    COMMAND test_calc --quick
    WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/data
)

WORKING_DIRECTORY matters more than you think. Tests that read data files break mysteriously when CTest runs them from the build root instead of the test folder. Set it explicitly.

Enabling and disabling tests per build

top-level CMakeLists.txt
include(CTest) # defines BUILD_TESTING (default ON) if(BUILD_TESTING) add_subdirectory(tests) endif()
cmake -S . -B build -DBUILD_TESTING=OFF    # build without tests

include(CTest) gives you the standard BUILD_TESTING switch, the ecosystem’s agreed knob. It also calls enable_testing() for you.

One test per executable, or discover

If you use GoogleTest, CTest can discover every TEST(...) inside your test binaries automatically, including new ones you add without touching CMake:

tests/CMakeLists.txt
include(FetchContent) # lesson 18 FetchContent_Declare(googletest GIT_REPOSITORY https://github.com/google/googletest.git GIT_TAG 703bd9caab50b139428cea1aaff9974ebee5742e ) FetchContent_MakeAvailable(googletest) add_executable(test_calc test_calc.cpp) target_link_libraries(test_calc PRIVATE calc GTest::gtest_main) # NEW in CMake 4.4. discover_tests() replaces the old # gtest_discover_tests() and include(GoogleTest) dance. discover_tests(test_calc)

Each TEST() in test_calc.cpp becomes its own CTest entry, with failure output on failure. discover_tests is new in CMake 4.4. The old module-based gtest_discover_tests still exists, but the new command is the intended path.

Running tests

ctest --test-dir build                       # all tests
ctest --test-dir build -R test_calc          # filter by name (regex)
ctest --test-dir build --output-on-failure   # show failing test output
ctest --test-dir build -j 8                  # run in parallel
ctest --test-dir build --preset dev          # presets from lesson 20

Pitfall

ctest without --test-dir must run from the build directory, or ctest finds nothing or tests the wrong build. Always pass --test-dir, or use a testPresets entry so ctest --preset handles it.

Tip

Make CI run exactly ctest --test-dir build --output-on-failure. The --output-on-failure flag is the difference between “CI failed, why?” and a log that tells you which assertion broke.

Read the docs