Google Test Integration
Google Test (GTest) is the most widely-used C++ testing framework. CMake provides first-class support through both FetchContent and find_package, plus the powerful gtest_discover_tests() for automatic test registration.
FetchContent with GTest
cmake_minimum_required(VERSION 3.24)
project(MyProject LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
include(CTest)
include(FetchContent)
# Fetch Google Test
FetchContent_Declare(
googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG v1.14.0
)
# Prevent GTest from overriding compiler/linker options on Windows
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(googletest)
# Build the library
add_library(calculator src/calculator.cpp)
target_include_directories(calculator PUBLIC include)
# Build tests
add_executable(test_calculator tests/test_calculator.cpp)
target_link_libraries(test_calculator PRIVATE
calculator
GTest::gtest_main # Provides main() automatically
GTest::gmock # Google Mock for mocking
)
# Register tests with CTest
include(GoogleTest)
gtest_discover_tests(test_calculator)
If Google Test is already installed on the system (e.g., via a package manager), use find_package:
cmake_minimum_required(VERSION 3.24)
project(MyProject LANGUAGES CXX)
include(CTest)
# Find system-installed GTest
find_package(GTest REQUIRED)
add_executable(test_math tests/test_math.cpp)
target_link_libraries(test_math PRIVATE
GTest::gtest_main
GTest::gmock
)
include(GoogleTest)
gtest_discover_tests(test_math)
gtest_discover_tests
The gtest_discover_tests() function (from the GoogleTest module) queries your test executable at build time to discover all TEST() and TEST_F() macros, registering each as a separate CTest test:
cmake_minimum_required(VERSION 3.24)
project(DiscoverDemo LANGUAGES CXX)
include(CTest)
include(FetchContent)
FetchContent_Declare(googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG v1.14.0
)
FetchContent_MakeAvailable(googletest)
add_executable(test_all
tests/test_math.cpp
tests/test_string.cpp
tests/test_container.cpp
)
target_link_libraries(test_all PRIVATE GTest::gtest_main)
include(GoogleTest)
# Discover all tests with custom options
gtest_discover_tests(test_all
# Prefix test names for clarity in ctest output
TEST_PREFIX "unit."
# Pass extra properties to all discovered tests
PROPERTIES
LABELS "unit"
TIMEOUT 30
# Set working directory for tests
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/tests
# Discovery timeout (increase for slow systems)
DISCOVERY_TIMEOUT 60
)
gtest_discover_tests runs at build time (not configure time), so it automatically picks up new TEST() macros without re-running CMake. This makes it far superior to the older gtest_add_tests() which required re-configuration.
Properties Propagation
// tests/test_math.cpp — Google Test example
#include <gtest/gtest.h>
#include "calculator.h"
TEST(Calculator, Addition) {
Calculator calc;
EXPECT_EQ(calc.add(2, 3), 5);
EXPECT_EQ(calc.add(-1, 1), 0);
EXPECT_EQ(calc.add(0, 0), 0);
}
TEST(Calculator, Division) {
Calculator calc;
EXPECT_DOUBLE_EQ(calc.divide(10, 2), 5.0);
EXPECT_THROW(calc.divide(1, 0), std::invalid_argument);
}
TEST(Calculator, DISABLED_SlowTest) {
// This test is disabled — won't run but will show as "Not Run"
Calculator calc;
// ... expensive computation
}
# After building, ctest sees individual tests:
$ ctest -N
Test #1: unit.Calculator.Addition
Test #2: unit.Calculator.Division
Test #3: unit.Calculator.DISABLED_SlowTest
# Run just the Calculator suite:
$ ctest -R "Calculator"
Catch2 Integration
Catch2 v3 is a modern header-light testing framework with BDD support and excellent integration with CMake:
cmake_minimum_required(VERSION 3.24)
project(Catch2Demo LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
include(CTest)
include(FetchContent)
# Fetch Catch2 v3
FetchContent_Declare(
Catch2
GIT_REPOSITORY https://github.com/catchorg/Catch2.git
GIT_TAG v3.5.4
)
FetchContent_MakeAvailable(Catch2)
# Build test executable
add_executable(test_app
tests/test_math.cpp
tests/test_strings.cpp
)
target_link_libraries(test_app PRIVATE Catch2::Catch2WithMain)
# Automatic test discovery (similar to gtest_discover_tests)
list(APPEND CMAKE_MODULE_PATH ${catch2_SOURCE_DIR}/extras)
include(Catch)
catch_discover_tests(test_app
TEST_PREFIX "catch."
REPORTER junit # Output JUnit XML for CI
)
Catch2 BDD Style Tests
// tests/test_strings.cpp — Catch2 BDD example
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_string.hpp>
#include "string_utils.h"
using Catch::Matchers::StartsWith;
using Catch::Matchers::EndsWith;
SCENARIO("String trimming removes whitespace", "[strings][trim]") {
GIVEN("A string with leading and trailing spaces") {
std::string input = " hello world ";
WHEN("trim is applied") {
auto result = trim(input);
THEN("leading and trailing spaces are removed") {
REQUIRE(result == "hello world");
REQUIRE_THAT(result, !StartsWith(" "));
REQUIRE_THAT(result, !EndsWith(" "));
}
}
}
GIVEN("A string with no whitespace") {
std::string input = "hello";
WHEN("trim is applied") {
auto result = trim(input);
THEN("the string is unchanged") {
REQUIRE(result == input);
}
}
}
}
Boost.Test Integration
If your project already uses Boost, Boost.Test integrates naturally:
cmake_minimum_required(VERSION 3.24)
project(BoostTestDemo LANGUAGES CXX)
include(CTest)
# Find Boost with the unit_test_framework component
find_package(Boost 1.80 REQUIRED COMPONENTS unit_test_framework)
add_executable(test_boost tests/test_boost.cpp)
target_link_libraries(test_boost PRIVATE Boost::unit_test_framework)
# Boost.Test doesn't have automatic discovery like GTest
# Register tests manually or use a wrapper
add_test(NAME boost_math COMMAND test_boost --run_test=MathSuite)
add_test(NAME boost_strings COMMAND test_boost --run_test=StringSuite)
set_tests_properties(boost_math boost_strings PROPERTIES
LABELS "boost"
TIMEOUT 30
)
// tests/test_boost.cpp
#define BOOST_TEST_MODULE MyProjectTests
#include <boost/test/unit_test.hpp>
#include "math_utils.h"
BOOST_AUTO_TEST_SUITE(MathSuite)
BOOST_AUTO_TEST_CASE(test_factorial) {
BOOST_CHECK_EQUAL(factorial(0), 1);
BOOST_CHECK_EQUAL(factorial(5), 120);
BOOST_CHECK_THROW(factorial(-1), std::invalid_argument);
}
BOOST_AUTO_TEST_SUITE_END()
doctest Integration
doctest is a single-header testing framework known for extremely fast compilation. It's ideal for projects where build time matters:
cmake_minimum_required(VERSION 3.24)
project(DoctestDemo LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
include(CTest)
include(FetchContent)
# Fetch doctest (header-only — very fast to fetch)
FetchContent_Declare(
doctest
GIT_REPOSITORY https://github.com/doctest/doctest.git
GIT_TAG v2.4.11
)
FetchContent_MakeAvailable(doctest)
add_executable(test_doctest tests/test_main.cpp tests/test_math.cpp)
target_link_libraries(test_doctest PRIVATE doctest::doctest)
# doctest provides its own discovery module
list(APPEND CMAKE_MODULE_PATH ${doctest_SOURCE_DIR}/scripts/cmake)
include(doctest)
doctest_discover_tests(test_doctest
TEST_PREFIX "dt."
)
// tests/test_math.cpp — doctest example
#include <doctest/doctest.h>
#include "math_utils.h"
TEST_CASE("Factorial computation") {
CHECK(factorial(0) == 1);
CHECK(factorial(1) == 1);
CHECK(factorial(5) == 120);
SUBCASE("Negative input throws") {
CHECK_THROWS_AS(factorial(-1), std::invalid_argument);
}
}
flowchart TD
A{Need mocking?} -->|Yes| B[Google Test + GMock]
A -->|No| C{Build time critical?}
C -->|Yes| D[doctest]
C -->|No| E{BDD style preferred?}
E -->|Yes| F[Catch2]
E -->|No| G{Already using Boost?}
G -->|Yes| H[Boost.Test]
G -->|No| I[Google Test]
style B fill:#d4edda,stroke:#28a745
style D fill:#d4edda,stroke:#28a745
style F fill:#d4edda,stroke:#28a745
style H fill:#d4edda,stroke:#28a745
style I fill:#d4edda,stroke:#28a745
Code Coverage
gcov, lcov, and gcovr
Code coverage measures which lines of your source code are exercised by tests. With GCC/Clang, use --coverage flags:
cmake_minimum_required(VERSION 3.24)
project(CoverageDemo LANGUAGES CXX)
include(CTest)
# Define a coverage build type
option(ENABLE_COVERAGE "Enable code coverage" OFF)
add_library(mylib src/mylib.cpp)
target_include_directories(mylib PUBLIC include)
if(ENABLE_COVERAGE)
# Add coverage flags to library and tests
target_compile_options(mylib PRIVATE --coverage -O0 -g)
target_link_options(mylib PRIVATE --coverage)
endif()
# Tests
add_executable(test_mylib tests/test_mylib.cpp)
target_link_libraries(test_mylib PRIVATE mylib GTest::gtest_main)
if(ENABLE_COVERAGE)
target_compile_options(test_mylib PRIVATE --coverage -O0 -g)
target_link_options(test_mylib PRIVATE --coverage)
endif()
include(GoogleTest)
gtest_discover_tests(test_mylib)
Custom Coverage Target
# Add a custom target to generate coverage reports
if(ENABLE_COVERAGE)
find_program(LCOV lcov REQUIRED)
find_program(GENHTML genhtml REQUIRED)
add_custom_target(coverage
# Clean previous data
COMMAND ${LCOV} --directory . --zerocounters
# Run tests
COMMAND ${CMAKE_CTEST_COMMAND} --output-on-failure
# Capture coverage data
COMMAND ${LCOV} --directory . --capture --output-file coverage.info
# Remove system headers and test files from report
COMMAND ${LCOV} --remove coverage.info
'/usr/*' '*/tests/*' '*/googletest/*'
--output-file coverage_filtered.info
# Generate HTML report
COMMAND ${GENHTML} coverage_filtered.info
--output-directory coverage_report
--title "MyLib Coverage"
--legend --show-details
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
COMMENT "Generating code coverage report..."
)
endif()
# Build with coverage enabled
cmake -B build -DENABLE_COVERAGE=ON -DCMAKE_BUILD_TYPE=Debug
cmake --build build
# Generate the coverage report
cmake --build build --target coverage
# Open the report
open build/coverage_report/index.html
If you prefer a Python-based tool that generates Cobertura XML (for CI) and HTML in one step:
if(ENABLE_COVERAGE)
find_program(GCOVR gcovr REQUIRED)
add_custom_target(coverage
COMMAND ${CMAKE_CTEST_COMMAND} --output-on-failure
COMMAND ${GCOVR}
--root ${CMAKE_SOURCE_DIR}
--exclude "tests/"
--html-details coverage_report/index.html
--xml coverage.xml
--print-summary
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
COMMENT "Generating coverage with gcovr..."
)
endif()
Memory Analysis
CTest has built-in support for memory checking tools like Valgrind and DrMemory:
cmake_minimum_required(VERSION 3.24)
project(MemcheckDemo LANGUAGES CXX)
include(CTest)
# Set memory check command (usually auto-detected if Valgrind is installed)
set(CTEST_MEMORYCHECK_COMMAND "/usr/bin/valgrind")
set(CTEST_MEMORYCHECK_COMMAND_OPTIONS
"--tool=memcheck --leak-check=full --show-leak-kinds=all --track-origins=yes"
)
# Suppression file for known acceptable leaks
set(CTEST_MEMORYCHECK_SUPPRESSIONS_FILE
"${CMAKE_SOURCE_DIR}/valgrind.supp"
)
add_executable(test_memory tests/test_memory.cpp)
target_link_libraries(test_memory PRIVATE mylib GTest::gtest_main)
include(GoogleTest)
gtest_discover_tests(test_memory)
# Run tests under Valgrind memcheck
ctest -T memcheck
# Or run Valgrind manually for more control
valgrind --tool=memcheck --leak-check=full \
--show-leak-kinds=all --track-origins=yes \
./build/test_memory
# On Windows, use DrMemory instead
# Set in CTestCustom.cmake or via:
# ctest -D CTEST_MEMORYCHECK_COMMAND="C:/DrMemory/bin/drmemory.exe" -T memcheck
AddressSanitizer with Tests
Sanitizers (ASan, UBSan, TSan) catch bugs at runtime with minimal overhead compared to Valgrind:
cmake_minimum_required(VERSION 3.24)
project(SanitizerDemo LANGUAGES CXX)
include(CTest)
option(ENABLE_ASAN "Enable AddressSanitizer" OFF)
option(ENABLE_UBSAN "Enable UndefinedBehaviorSanitizer" OFF)
option(ENABLE_TSAN "Enable ThreadSanitizer" OFF)
add_library(mylib src/mylib.cpp)
# Apply sanitizer flags globally
if(ENABLE_ASAN)
add_compile_options(-fsanitize=address -fno-omit-frame-pointer -O1 -g)
add_link_options(-fsanitize=address)
endif()
if(ENABLE_UBSAN)
add_compile_options(-fsanitize=undefined -fno-omit-frame-pointer -O1 -g)
add_link_options(-fsanitize=undefined)
endif()
if(ENABLE_TSAN)
add_compile_options(-fsanitize=thread -O1 -g)
add_link_options(-fsanitize=thread)
endif()
# Tests
add_executable(test_mylib tests/test_mylib.cpp)
target_link_libraries(test_mylib PRIVATE mylib GTest::gtest_main)
include(GoogleTest)
gtest_discover_tests(test_mylib)
# Build with AddressSanitizer
cmake -B build-asan -DENABLE_ASAN=ON
cmake --build build-asan
cd build-asan && ctest --output-on-failure
# Build with UndefinedBehaviorSanitizer
cmake -B build-ubsan -DENABLE_UBSAN=ON
cmake --build build-ubsan
cd build-ubsan && ctest --output-on-failure
Test Organization Patterns
For large projects, organize tests into separate targets by scope:
cmake_minimum_required(VERSION 3.24)
project(OrganizedTests LANGUAGES CXX)
include(CTest)
include(FetchContent)
FetchContent_Declare(googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG v1.14.0
)
FetchContent_MakeAvailable(googletest)
include(GoogleTest)
# ─── Unit Tests (fast, isolated) ─────────────────────────────
add_executable(test_unit
tests/unit/test_math.cpp
tests/unit/test_string.cpp
tests/unit/test_container.cpp
)
target_link_libraries(test_unit PRIVATE mylib GTest::gtest_main)
gtest_discover_tests(test_unit
TEST_PREFIX "unit."
PROPERTIES LABELS "unit" TIMEOUT 10
)
# ─── Integration Tests (slower, may need resources) ──────────
add_executable(test_integration
tests/integration/test_database.cpp
tests/integration/test_api.cpp
)
target_link_libraries(test_integration PRIVATE mylib GTest::gtest_main)
gtest_discover_tests(test_integration
TEST_PREFIX "integ."
PROPERTIES LABELS "integration" TIMEOUT 60
)
# ─── Benchmark Tests (separate, not in default CTest run) ────
add_executable(bench_mylib tests/bench/bench_main.cpp)
target_link_libraries(bench_mylib PRIVATE mylib)
# Don't register with CTest — run manually
In a multi-library project, each library gets its own test executable:
project/
├── libs/
│ ├── core/
│ │ ├── CMakeLists.txt # add_library(core ...)
│ │ ├── src/
│ │ └── tests/
│ │ ├── CMakeLists.txt # add_executable(test_core ...)
│ │ └── test_core.cpp
│ └── network/
│ ├── CMakeLists.txt # add_library(network ...)
│ ├── src/
│ └── tests/
│ ├── CMakeLists.txt # add_executable(test_network ...)
│ └── test_network.cpp
└── CMakeLists.txt # Top-level
# libs/core/tests/CMakeLists.txt
add_executable(test_core test_core.cpp)
target_link_libraries(test_core PRIVATE core GTest::gtest_main)
gtest_discover_tests(test_core TEST_PREFIX "core.")
# Run only core tests:
# ctest -R "^core\."
Continuous Integration
Running CMake tests in CI pipelines with proper reporting:
# .github/workflows/test.yml — GitHub Actions example
name: CMake Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Configure
run: cmake -B build -DCMAKE_BUILD_TYPE=Release
- name: Build
run: cmake --build build -j$(nproc)
- name: Test
run: |
cd build
ctest --output-on-failure --output-junit test-results.xml
- name: Upload Test Results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results
path: build/test-results.xml
# GitLab CI example (.gitlab-ci.yml)
test:
stage: test
image: gcc:13
script:
- cmake -B build -DCMAKE_BUILD_TYPE=Debug -DENABLE_COVERAGE=ON
- cmake --build build -j$(nproc)
- cd build && ctest --output-on-failure --output-junit report.xml
- gcovr --root .. --exclude "tests/" --xml coverage.xml
artifacts:
reports:
junit: build/report.xml
coverage_report:
coverage_format: cobertura
path: build/coverage.xml
flowchart LR
A[Push / PR] --> B[Configure]
B --> C[Build]
C --> D[Unit Tests]
C --> E[Integration Tests]
D --> F[Coverage Report]
E --> F
F --> G{All Pass?}
G -->|Yes| H[Deploy / Merge]
G -->|No| I[Notify / Block]
style A fill:#f8f9fa,stroke:#3B9797
style H fill:#d4edda,stroke:#28a745
style I fill:#f8d7da,stroke:#dc3545
# CTest output formats for CI:
ctest --output-junit results.xml # JUnit XML (GitHub Actions, GitLab)
ctest --output-on-failure # Print stdout/stderr only for failures
ctest -T Test # CDash submission format
ctest --test-output-size-passed 65536 # Limit output size per test