- C++ Testing (Agent Skill)
- Agent-focused testing workflow for modern C++ (C++17/20) using GoogleTest/GoogleMock with CMake/CTest.
- When to Use
- Writing new C++ tests or fixing existing tests
- Designing unit/integration test coverage for C++ components
- Adding test coverage, CI gating, or regression protection
- Configuring CMake/CTest workflows for consistent execution
- Investigating test failures or flaky behavior
- Enabling sanitizers for memory/race diagnostics
- When NOT to Use
- Implementing new product features without test changes
- Large-scale refactors unrelated to test coverage or failures
- Performance tuning without test regressions to validate
- Non-C++ projects or non-test tasks
- Core Concepts
- TDD loop
-
- red → green → refactor (tests first, minimal fix, then cleanups).
- Isolation
-
- prefer dependency injection and fakes over global state.
- Test layout
- :
- tests/unit
- ,
- tests/integration
- ,
- tests/testdata
- .
- Mocks vs fakes
-
- mock for interactions, fake for stateful behavior.
- CTest discovery
-
- use
- gtest_discover_tests()
- for stable test discovery.
- CI signal
-
- run subset first, then full suite with
- --output-on-failure
- .
- TDD Workflow
- Follow the RED → GREEN → REFACTOR loop:
- RED
-
- write a failing test that captures the new behavior
- GREEN
-
- implement the smallest change to pass
- REFACTOR
- clean up while tests stay green // tests/add_test.cpp
include
include
include
include
include
include
/users/ ) { } std :: optional < User
Find ( const std :: string & /name/ ) { return User { "alice" } ; } } ; class UserStoreTest : public :: testing :: Test { protected : void SetUp ( ) override { store = std :: make_unique < UserStore
( ":memory:" ) ; store -> Seed ( { { "alice" } , { "bob" } } ) ; } std :: unique_ptr < UserStore
store ; } ; TEST_F ( UserStoreTest , FindsExistingUser ) { auto user = store -> Find ( "alice" ) ; ASSERT_TRUE ( user . has_value ( ) ) ; EXPECT_EQ ( user -> name , "alice" ) ; } Mock (gmock) // tests/notifier_test.cpp
include
include
include
CMakeLists.txt (excerpt)
cmake_minimum_required ( VERSION 3.20 ) project ( example LANGUAGES CXX ) set ( CMAKE_CXX_STANDARD 20 ) set ( CMAKE_CXX_STANDARD_REQUIRED ON ) include ( FetchContent )
Prefer project-locked versions. If using a tag, use a pinned version per project policy.
set ( GTEST_VERSION v1. 17.0 )
Adjust to project policy.
FetchContent_Declare ( googletest
Google Test framework (official repository)
- URL https://github.com/google/googletest/archive/refs/tags/
- ${
- GTEST_VERSION
- }
- .zip
- )
- FetchContent_MakeAvailable
- (
- googletest
- )
- add_executable
- (
- example_tests
- tests/calculator_test.cpp
- src/calculator.cpp
- )
- target_link_libraries
- (
- example_tests
- GTest::gtest
- GTest::gmock
- GTest::gtest_main
- )
- enable_testing
- (
- )
- include
- (
- GoogleTest
- )
- gtest_discover_tests
- (
- example_tests
- )
- cmake
- -S
- .
- -B
- build
- -DCMAKE_BUILD_TYPE
- =
- Debug
- cmake
- --build
- build
- -j
- ctest --test-dir build --output-on-failure
- Running Tests
- ctest --test-dir build --output-on-failure
- ctest --test-dir build
- -R
- ClampTest
- ctest --test-dir build
- -R
- "UserStoreTest.*"
- --output-on-failure
- ./build/example_tests
- --gtest_filter
- =
- ClampTest.*
- ./build/example_tests
- --gtest_filter
- =
- UserStoreTest.FindsExistingUser
- Debugging Failures
- Re-run the single failing test with gtest filter.
- Add scoped logging around the failing assertion.
- Re-run with sanitizers enabled.
- Expand to full suite once the root cause is fixed.
- Coverage
- Prefer target-level settings instead of global flags.
- option
- (
- ENABLE_COVERAGE
- "Enable coverage flags"
- OFF
- )
- if
- (
- ENABLE_COVERAGE
- )
- if
- (
- CMAKE_CXX_COMPILER_ID
- MATCHES
- "GNU"
- )
- target_compile_options
- (
- example_tests
- PRIVATE
- --coverage
- )
- target_link_options
- (
- example_tests
- PRIVATE
- --coverage
- )
- elseif
- (
- CMAKE_CXX_COMPILER_ID
- MATCHES
- "Clang"
- )
- target_compile_options
- (
- example_tests
- PRIVATE
- -fprofile-instr-generate -fcoverage-mapping
- )
- target_link_options
- (
- example_tests
- PRIVATE
- -fprofile-instr-generate
- )
- endif
- (
- )
- endif
- (
- )
- GCC + gcov + lcov:
- cmake
- -S
- .
- -B
- build-cov
- -DENABLE_COVERAGE
- =
- ON
- cmake
- --build
- build-cov
- -j
- ctest --test-dir build-cov
- lcov
- --capture
- --directory
- build-cov --output-file coverage.info
- lcov
- --remove
- coverage.info
- '/usr/*'
- --output-file coverage.info
- genhtml coverage.info --output-directory coverage
- Clang + llvm-cov:
- cmake
- -S
- .
- -B
- build-llvm
- -DENABLE_COVERAGE
- =
- ON
- -DCMAKE_CXX_COMPILER
- =
- clang++
- cmake
- --build
- build-llvm
- -j
- LLVM_PROFILE_FILE
- =
- "build-llvm/default.profraw"
- ctest --test-dir build-llvm
- llvm-profdata merge
- -sparse
- build-llvm/default.profraw
- -o
- build-llvm/default.profdata
- llvm-cov report build-llvm/example_tests -instr-profile
- =
- build-llvm/default.profdata
- Sanitizers
- option
- (
- ENABLE_ASAN
- "Enable AddressSanitizer"
- OFF
- )
- option
- (
- ENABLE_UBSAN
- "Enable UndefinedBehaviorSanitizer"
- OFF
- )
- option
- (
- ENABLE_TSAN
- "Enable ThreadSanitizer"
- OFF
- )
- if
- (
- ENABLE_ASAN
- )
- add_compile_options
- (
- -fsanitize=address -fno-omit-frame-pointer
- )
- add_link_options
- (
- -fsanitize=address
- )
- endif
- (
- )
- if
- (
- ENABLE_UBSAN
- )
- add_compile_options
- (
- -fsanitize=undefined -fno-omit-frame-pointer
- )
- add_link_options
- (
- -fsanitize=undefined
- )
- endif
- (
- )
- if
- (
- ENABLE_TSAN
- )
- add_compile_options
- (
- -fsanitize=thread
- )
- add_link_options
- (
- -fsanitize=thread
- )
- endif
- (
- )
- Flaky Tests Guardrails
- Never use
- sleep
- for synchronization; use condition variables or latches.
- Make temp directories unique per test and always clean them.
- Avoid real time, network, or filesystem dependencies in unit tests.
- Use deterministic seeds for randomized inputs.
- Best Practices
- DO
- Keep tests deterministic and isolated
- Prefer dependency injection over globals
- Use
- ASSERT_*
- for preconditions,
- EXPECT_*
- for multiple checks
- Separate unit vs integration tests in CTest labels or directories
- Run sanitizers in CI for memory and race detection
- DON'T
- Don't depend on real time or network in unit tests
- Don't use sleeps as synchronization when a condition variable can be used
- Don't over-mock simple value objects
- Don't use brittle string matching for non-critical logs
- Common Pitfalls
- Using fixed temp paths
- → Generate unique temp directories per test and clean them.
- Relying on wall clock time
- → Inject a clock or use fake time sources.
- Flaky concurrency tests
- → Use condition variables/latches and bounded waits.
- Hidden global state
- → Reset global state in fixtures or remove globals.
- Over-mocking
- → Prefer fakes for stateful behavior and only mock interactions.
- Missing sanitizer runs
- → Add ASan/UBSan/TSan builds in CI.
- Coverage on debug-only builds
- → Ensure coverage targets use consistent flags.
- Optional Appendix: Fuzzing / Property Testing
- Only use if the project already supports LLVM/libFuzzer or a property-testing library.
- libFuzzer
-
- best for pure functions with minimal I/O.
- RapidCheck
- property-based tests to validate invariants. Minimal libFuzzer harness (pseudocode: replace ParseConfig):
include
include
- include
- extern
- "C"
- int
- LLVMFuzzerTestOneInput
- (
- const
- uint8_t
- *
- data
- ,
- size_t size
- )
- {
- std
- ::
- string
- input
- (
- reinterpret_cast
- <
- const
- char
- *
- >
- (
- data
- )
- ,
- size
- )
- ;
- // ParseConfig(input); // project function
- return
- 0
- ;
- }
- Alternatives to GoogleTest
- Catch2
-
- header-only, expressive matchers
- doctest
- lightweight, minimal compile overhead