Testing workflow for modern C++ (C++17/20) with GoogleTest / GoogleMock driven through CMake and CTest. Covers the red-green-refactor loop, unit and integration test layout, fixtures, mocks vs fakes, dependency injection for isolation, stable test discovery with gtest_discover_tests, coverage collection (gcov/lcov and llvm-cov), the AddressSanitizer / UBSan / ThreadSanitizer builds, and guardrails that keep tests deterministic and non-flaky. Use when writing or fixing C++ tests, wiring a CMak...
Scanned 9/2/2026
Install to Claude Code
npx -y skills add Canhada-Labs/ceo-orchestration --skill cpp-testing --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Cpp Testing?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/canhada-labs-cpp-testing)More formats (shields.io, HTML) on the badges page.
---
name: cpp-testing
description: >
Testing workflow for modern C++ (C++17/20) with GoogleTest / GoogleMock
driven through CMake and CTest. Covers the red-green-refactor loop, unit
and integration test layout, fixtures, mocks vs fakes, dependency
injection for isolation, stable test discovery with gtest_discover_tests,
coverage collection (gcov/lcov and llvm-cov), the AddressSanitizer /
UBSan / ThreadSanitizer builds, and guardrails that keep tests
deterministic and non-flaky. Use when writing or fixing C++ tests, wiring
a CMake/CTest test target, diagnosing a failing or flaky test, or adding
coverage or sanitizer gates. Not for feature work with no test change,
large refactors unrelated to tests, or non-C++ projects.
metadata:
activation_triggers:
- "writing or reviewing C++ unit, integration, or fuzz tests"
- "GoogleTest / GoogleMock usage (TEST, TEST_F, TEST_P, EXPECT_CALL, MOCK_METHOD)"
- "wiring CTest, gtest_discover_tests, or sanitizer jobs into CMake/CI"
- "diagnosing a flaky C++ test or raising branch coverage"
paths:
- "**/*_test.cpp"
- "**/*_test.cc"
- "**/*Test.cpp"
- "**/tests/**/*.cpp"
- "**/tests/**/*.cc"
- "**/test/**/*.cpp"
- "**/test/**/*.cc"
version: 1.0.0
risk_class: low
source: affaan-m/ecc@81af4076 skills/cpp-testing/
license: MIT
---
# C++ Testing
An agent-facing testing workflow for modern C++ using GoogleTest and
GoogleMock, built and run through CMake with CTest. The goal is a suite that
is deterministic, isolated, fast to iterate on, and honest under sanitizers.
## When to Activate
Use this skill when you are:
- Writing new C++ tests or repairing existing ones.
- Designing unit or integration coverage for a component.
- Wiring a CMake/CTest test target or CI test gate.
- Chasing down a failing or flaky test.
- Turning on coverage measurement or a sanitizer build.
Do not reach for it when the change ships a feature with no test delta, on a
broad refactor with no test-coverage or failure motive, or on a non-C++
project.
## Core Ideas
- **Red-green-refactor.** Write the failing test first, make it pass with
the smallest change, then clean up while it stays green.
- **Isolation by injection.** Prefer dependency injection and fakes to
global state; a test should not reach outside its own scope.
- **A predictable layout.** Keep `tests/unit`, `tests/integration`, and
`tests/testdata` separate so intent and runtime are obvious.
- **Mock vs fake.** Mock when you assert on interactions (a call happened,
with these arguments); fake when you need stateful behavior stood in for
a real collaborator.
- **Stable discovery.** Let `gtest_discover_tests()` enumerate tests at
build time so CTest sees each case individually.
- **A tiered CI signal.** Run the fast subset first, then the full suite
with `--output-on-failure`.
## The Red-Green-Refactor Loop
1. **Red** — write a test that fails because the behavior does not exist yet.
2. **Green** — add the minimum production code to make it pass.
3. **Refactor** — improve names and structure while the test stays green.
```cpp
// tests/unit/clamp_test.cpp (RED — clamp_to_range does not exist yet)
#include <gtest/gtest.h>
int clamp_to_range(int value, int lo, int hi); // declared by production code
TEST(ClampTest, PullsValueUpToLowerBound) {
EXPECT_EQ(clamp_to_range(-5, 0, 10), 0);
}
// src/clamp.cpp (GREEN — smallest change that passes)
int clamp_to_range(int value, int lo, int hi) {
if (value < lo) return lo;
if (value > hi) return hi;
return value;
}
// REFACTOR once green: e.g. delegate to std::clamp and delete the branches.
```
## Test Shapes
### A plain unit test
```cpp
// tests/unit/tokenizer_test.cpp
#include <gtest/gtest.h>
std::size_t count_tokens(std::string_view line); // production code
TEST(TokenizerTest, CountsSpaceSeparatedWords) {
EXPECT_EQ(count_tokens("alpha beta gamma"), 3u);
}
TEST(TokenizerTest, TreatsEmptyLineAsZeroTokens) {
EXPECT_EQ(count_tokens(""), 0u);
}
```
Use `ASSERT_*` for a precondition that must hold before the rest of the test
makes sense (it aborts the test on failure); use `EXPECT_*` for independent
checks you want all reported in one run.
### A fixture
A fixture shares setup across related tests. Rebuild state in `SetUp()` so
each test starts clean. The types below are illustrative stand-ins — replace
them with your own.
```cpp
// tests/unit/account_store_test.cpp
#include <gtest/gtest.h>
#include <memory>
#include <optional>
#include <string>
struct Account { std::string owner; long cents; };
class AccountStore { // stand-in for the real store
public:
explicit AccountStore(std::string /*dsn*/) {}
void seed(std::initializer_list<Account>) {}
std::optional<Account> find(const std::string& owner) {
return Account{owner, 0};
}
};
class AccountStoreTest : public ::testing::Test {
protected:
void SetUp() override {
store_ = std::make_unique<AccountStore>(":memory:");
store_->seed({{"ana", 100}, {"bruno", 250}});
}
std::unique_ptr<AccountStore> store_;
};
TEST_F(AccountStoreTest, FindsASeededAccount) {
auto found = store_->find("ana");
ASSERT_TRUE(found.has_value()); // precondition — abort if absent
EXPECT_EQ(found->owner, "ana");
}
```
### A mock (GoogleMock)
Mock an interface to assert on the interaction, then inject the mock through
a reference or pointer.
```cpp
// tests/unit/dispatcher_test.cpp
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <string>
class Sink {
public:
virtual ~Sink() = default;
virtual void emit(const std::string& event) = 0;
};
class MockSink : public Sink {
public:
MOCK_METHOD(void, emit, (const std::string& event), (override));
};
class Dispatcher {
public:
explicit Dispatcher(Sink& sink) : sink_(sink) {}
void fire(const std::string& event) { sink_.emit(event); }
private:
Sink& sink_;
};
TEST(DispatcherTest, ForwardsExactlyOneEvent) {
MockSink sink;
Dispatcher dispatcher(sink);
EXPECT_CALL(sink, emit("started")).Times(1);
dispatcher.fire("started");
}
```
Reach for a mock only when the interaction *is* the behavior under test. For
a stateful collaborator, a hand-written fake is usually clearer and less
brittle than layers of `EXPECT_CALL`.
## Build and Run with CMake / CTest
Pin the framework version to a project-controlled value rather than a moving
tag, so builds are reproducible.
```cmake
# 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)
set(GTEST_TAG v1.15.2) # pin per project policy; bump deliberately
FetchContent_Declare(
googletest
URL https://github.com/google/googletest/archive/refs/tags/${GTEST_TAG}.zip
)
FetchContent_MakeAvailable(googletest)
add_executable(example_tests
tests/unit/clamp_test.cpp
src/clamp.cpp
)
target_link_libraries(example_tests
PRIVATE GTest::gtest GTest::gmock GTest::gtest_main)
enable_testing()
include(GoogleTest)
gtest_discover_tests(example_tests) # each TEST becomes a CTest case
```
```bash
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
cmake --build build -j
ctest --test-dir build --output-on-failure
```
Filter to a subset while iterating — through CTest by name, or through the
gtest binary directly:
```bash
ctest --test-dir build -R "AccountStoreTest.*" --output-on-failure
./build/example_tests --gtest_filter=DispatcherTest.ForwardsExactlyOneEvent
```
## Debugging a Failure
1. Re-run just the failing case with a `--gtest_filter` narrow to it.
2. Add scoped logging around the failing assertion; prefer
`SCOPED_TRACE` to attach context to a helper that runs multiple checks.
3. Re-run under a sanitizer (below) — many "logic" failures are actually
memory or race bugs surfacing non-deterministically.
4. Only after the root cause is understood, widen back to the full suite.
## Coverage
Prefer per-target coverage flags to global ones, so instrumentation is
scoped to the code under test.
```cmake
option(ENABLE_COVERAGE "Enable coverage instrumentation" 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 with gcov and lcov:
```bash
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-html
```
Clang with llvm-cov:
```bash
cmake -S . -B build-llvm -DENABLE_COVERAGE=ON -DCMAKE_CXX_COMPILER=clang++
cmake --build build-llvm -j
LLVM_PROFILE_FILE="build-llvm/run.profraw" ctest --test-dir build-llvm
llvm-profdata merge -sparse build-llvm/run.profraw -o build-llvm/run.profdata
llvm-cov report build-llvm/example_tests -instr-profile=build-llvm/run.profdata
```
Keep coverage builds consistent with test builds — measuring coverage on a
debug-only configuration that the suite never otherwise exercises produces
misleading numbers.
## Sanitizers
Run the suite under sanitizers in CI. They catch use-after-free, buffer
overflow, undefined behavior, and data races that a passing assertion hides.
```cmake
option(ENABLE_ASAN "AddressSanitizer" OFF)
option(ENABLE_UBSAN "UndefinedBehaviorSanitizer" OFF)
option(ENABLE_TSAN "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) # TSan is exclusive of ASan
add_link_options(-fsanitize=thread)
endif()
```
ASan and TSan cannot be combined in one binary — build a separate
configuration for each. UBSan pairs with ASan.
## Keeping Tests Non-Flaky
Flakiness is a correctness defect in the test, not bad luck. The common
causes and their fixes:
- **Sleeps used for synchronization** → wait on a condition variable, a
latch, or a future — never `sleep` to "let it settle."
- **Fixed temp paths** → generate a unique temp directory per test and
clean it up in teardown; two parallel tests must not collide.
- **Wall-clock or timezone dependence** → inject a clock or use a fake time
source; never assert on `now()`.
- **Real network or filesystem in a unit test** → fake the boundary; keep
real I/O in clearly labelled integration tests.
- **Randomized input with a fresh seed each run** → fix the seed so a
failure reproduces.
- **Hidden global state leaking between tests** → reset it in the fixture,
or remove the global.
## Best Practices
**Do**
- Keep every test deterministic and independent of run order.
- Inject collaborators; avoid singletons and globals in test paths.
- Use `ASSERT_*` for preconditions, `EXPECT_*` for independent checks.
- Split unit from integration via directories or CTest labels.
- Gate CI on a sanitizer build for memory and race coverage.
**Don't**
- Depend on real time, network, or a shared writable filesystem in a unit
test.
- Use a sleep where a condition variable expresses the wait.
- Over-mock plain value objects — construct them directly.
- Assert on brittle substrings of non-critical log output.
## Optional: Fuzzing and Property Testing
Only when the project already supports the tooling.
- **libFuzzer** suits pure functions with little I/O — a parser, a decoder.
- **RapidCheck** (or similar) expresses invariants as properties checked
across generated inputs.
A minimal libFuzzer entry point (replace the parse call with yours):
```cpp
#include <cstddef>
#include <cstdint>
#include <string>
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
std::string input(reinterpret_cast<const char*>(data), size);
// parse_config(input); // exercise the function under test
return 0;
}
```
## Alternatives to GoogleTest
- **Catch2** — header-only, expressive matchers, `SECTION`-based fixtures.
- **doctest** — very lightweight, minimal compile overhead, easy to embed.
## Changelog
- **1.0.0** — Initial release. GoogleTest/GoogleMock testing workflow on
CMake/CTest: red-green-refactor, unit/fixture/mock shapes, build-and-run,
failure debugging, coverage (gcov/lcov and llvm-cov), ASan/UBSan/TSan
builds, anti-flakiness guardrails, and optional fuzz/property testing.
Skill-Import-Attestation: reviewed-by=AE9B236FDAF0462874060C6BCFCFACF00335DC74; sha256=8a0abf6c01614011d66558a3191ff2e6257e638600173c69bd2178c63031380a
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!