Skip to content

Commit 3bc8672

Browse files
authored
Merge pull request anomalyco#183 from moonlandar/feat/cpp-testing-skill
Feat/cpp testing skill
2 parents 0278224 + a5ec19c commit 3bc8672

3 files changed

Lines changed: 324 additions & 0 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@ everything-claude-code/
218218
| |-- verification-loop/ # Continuous verification (Longform Guide)
219219
| |-- golang-patterns/ # Go idioms and best practices
220220
| |-- golang-testing/ # Go testing patterns, TDD, benchmarks
221+
| |-- cpp-testing/ # C++ testing with GoogleTest, CMake/CTest (NEW)
221222
| |-- django-patterns/ # Django patterns, models, views (NEW)
222223
| |-- django-security/ # Django security best practices (NEW)
223224
| |-- django-tdd/ # Django TDD workflow (NEW)

‎README.zh-CN.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ everything-claude-code/
171171
| |-- verification-loop/ # 持续验证(详细指南)
172172
| |-- golang-patterns/ # Go 惯用语和最佳实践(新增)
173173
| |-- golang-testing/ # Go 测试模式、TDD、基准测试(新增)
174+
| |-- cpp-testing/ # C++ 测试模式、GoogleTest、CMake/CTest(新增)
174175
|
175176
|-- commands/ # 用于快速执行的斜杠命令
176177
| |-- tdd.md # /tdd - 测试驱动开发

‎skills/cpp-testing/SKILL.md‎

Lines changed: 322 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,322 @@
1+
---
2+
name: cpp-testing
3+
description: Use only when writing/updating/fixing C++ tests, configuring GoogleTest/CTest, diagnosing failing or flaky tests, or adding coverage/sanitizers.
4+
---
5+
6+
# C++ Testing (Agent Skill)
7+
8+
Agent-focused testing workflow for modern C++ (C++17/20) using GoogleTest/GoogleMock with CMake/CTest.
9+
10+
## When to Use
11+
12+
- Writing new C++ tests or fixing existing tests
13+
- Designing unit/integration test coverage for C++ components
14+
- Adding test coverage, CI gating, or regression protection
15+
- Configuring CMake/CTest workflows for consistent execution
16+
- Investigating test failures or flaky behavior
17+
- Enabling sanitizers for memory/race diagnostics
18+
19+
### When NOT to Use
20+
21+
- Implementing new product features without test changes
22+
- Large-scale refactors unrelated to test coverage or failures
23+
- Performance tuning without test regressions to validate
24+
- Non-C++ projects or non-test tasks
25+
26+
## Core Concepts
27+
28+
- **TDD loop**: red → green → refactor (tests first, minimal fix, then cleanups).
29+
- **Isolation**: prefer dependency injection and fakes over global state.
30+
- **Test layout**: `tests/unit`, `tests/integration`, `tests/testdata`.
31+
- **Mocks vs fakes**: mock for interactions, fake for stateful behavior.
32+
- **CTest discovery**: use `gtest_discover_tests()` for stable test discovery.
33+
- **CI signal**: run subset first, then full suite with `--output-on-failure`.
34+
35+
## TDD Workflow
36+
37+
Follow the RED → GREEN → REFACTOR loop:
38+
39+
1. **RED**: write a failing test that captures the new behavior
40+
2. **GREEN**: implement the smallest change to pass
41+
3. **REFACTOR**: clean up while tests stay green
42+
43+
```cpp
44+
// tests/add_test.cpp
45+
#include <gtest/gtest.h>
46+
47+
int Add(int a, int b); // Provided by production code.
48+
49+
TEST(AddTest, AddsTwoNumbers) { // RED
50+
EXPECT_EQ(Add(2, 3), 5);
51+
}
52+
53+
// src/add.cpp
54+
int Add(int a, int b) { // GREEN
55+
return a + b;
56+
}
57+
58+
// REFACTOR: simplify/rename once tests pass
59+
```
60+
61+
## Code Examples
62+
63+
### Basic Unit Test (gtest)
64+
65+
```cpp
66+
// tests/calculator_test.cpp
67+
#include <gtest/gtest.h>
68+
69+
int Add(int a, int b); // Provided by production code.
70+
71+
TEST(CalculatorTest, AddsTwoNumbers) {
72+
EXPECT_EQ(Add(2, 3), 5);
73+
}
74+
```
75+
76+
### Fixture (gtest)
77+
78+
```cpp
79+
// tests/user_store_test.cpp
80+
// Pseudocode stub: replace UserStore/User with project types.
81+
#include <gtest/gtest.h>
82+
#include <memory>
83+
#include <optional>
84+
#include <string>
85+
86+
struct User { std::string name; };
87+
class UserStore {
88+
public:
89+
explicit UserStore(std::string /*path*/) {}
90+
void Seed(std::initializer_list<User> /*users*/) {}
91+
std::optional<User> Find(const std::string &/*name*/) { return User{"alice"}; }
92+
};
93+
94+
class UserStoreTest : public ::testing::Test {
95+
protected:
96+
void SetUp() override {
97+
store = std::make_unique<UserStore>(":memory:");
98+
store->Seed({{"alice"}, {"bob"}});
99+
}
100+
101+
std::unique_ptr<UserStore> store;
102+
};
103+
104+
TEST_F(UserStoreTest, FindsExistingUser) {
105+
auto user = store->Find("alice");
106+
ASSERT_TRUE(user.has_value());
107+
EXPECT_EQ(user->name, "alice");
108+
}
109+
```
110+
111+
### Mock (gmock)
112+
113+
```cpp
114+
// tests/notifier_test.cpp
115+
#include <gmock/gmock.h>
116+
#include <gtest/gtest.h>
117+
#include <string>
118+
119+
class Notifier {
120+
public:
121+
virtual ~Notifier() = default;
122+
virtual void Send(const std::string &message) = 0;
123+
};
124+
125+
class MockNotifier : public Notifier {
126+
public:
127+
MOCK_METHOD(void, Send, (const std::string &message), (override));
128+
};
129+
130+
class Service {
131+
public:
132+
explicit Service(Notifier &notifier) : notifier_(notifier) {}
133+
void Publish(const std::string &message) { notifier_.Send(message); }
134+
135+
private:
136+
Notifier &notifier_;
137+
};
138+
139+
TEST(ServiceTest, SendsNotifications) {
140+
MockNotifier notifier;
141+
Service service(notifier);
142+
143+
EXPECT_CALL(notifier, Send("hello")).Times(1);
144+
service.Publish("hello");
145+
}
146+
```
147+
148+
### CMake/CTest Quickstart
149+
150+
```cmake
151+
# CMakeLists.txt (excerpt)
152+
cmake_minimum_required(VERSION 3.20)
153+
project(example LANGUAGES CXX)
154+
155+
set(CMAKE_CXX_STANDARD 20)
156+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
157+
158+
include(FetchContent)
159+
# Prefer project-locked versions. If using a tag, use a pinned version per project policy.
160+
set(GTEST_VERSION v1.17.0) # Adjust to project policy.
161+
FetchContent_Declare(
162+
googletest
163+
URL https://github.com/google/googletest/archive/refs/tags/${GTEST_VERSION}.zip
164+
)
165+
FetchContent_MakeAvailable(googletest)
166+
167+
add_executable(example_tests
168+
tests/calculator_test.cpp
169+
src/calculator.cpp
170+
)
171+
target_link_libraries(example_tests GTest::gtest GTest::gmock GTest::gtest_main)
172+
173+
enable_testing()
174+
include(GoogleTest)
175+
gtest_discover_tests(example_tests)
176+
```
177+
178+
```bash
179+
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
180+
cmake --build build -j
181+
ctest --test-dir build --output-on-failure
182+
```
183+
184+
## Running Tests
185+
186+
```bash
187+
ctest --test-dir build --output-on-failure
188+
ctest --test-dir build -R ClampTest
189+
ctest --test-dir build -R "UserStoreTest.*" --output-on-failure
190+
```
191+
192+
```bash
193+
./build/example_tests --gtest_filter=ClampTest.*
194+
./build/example_tests --gtest_filter=UserStoreTest.FindsExistingUser
195+
```
196+
197+
## Debugging Failures
198+
199+
1. Re-run the single failing test with gtest filter.
200+
2. Add scoped logging around the failing assertion.
201+
3. Re-run with sanitizers enabled.
202+
4. Expand to full suite once the root cause is fixed.
203+
204+
## Coverage
205+
206+
Prefer target-level settings instead of global flags.
207+
208+
```cmake
209+
option(ENABLE_COVERAGE "Enable coverage flags" OFF)
210+
211+
if(ENABLE_COVERAGE)
212+
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU")
213+
target_compile_options(example_tests PRIVATE --coverage)
214+
target_link_options(example_tests PRIVATE --coverage)
215+
elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
216+
target_compile_options(example_tests PRIVATE -fprofile-instr-generate -fcoverage-mapping)
217+
target_link_options(example_tests PRIVATE -fprofile-instr-generate)
218+
endif()
219+
endif()
220+
```
221+
222+
GCC + gcov + lcov:
223+
224+
```bash
225+
cmake -S . -B build-cov -DENABLE_COVERAGE=ON
226+
cmake --build build-cov -j
227+
ctest --test-dir build-cov
228+
lcov --capture --directory build-cov --output-file coverage.info
229+
lcov --remove coverage.info '/usr/*' --output-file coverage.info
230+
genhtml coverage.info --output-directory coverage
231+
```
232+
233+
Clang + llvm-cov:
234+
235+
```bash
236+
cmake -S . -B build-llvm -DENABLE_COVERAGE=ON -DCMAKE_CXX_COMPILER=clang++
237+
cmake --build build-llvm -j
238+
LLVM_PROFILE_FILE="build-llvm/default.profraw" ctest --test-dir build-llvm
239+
llvm-profdata merge -sparse build-llvm/default.profraw -o build-llvm/default.profdata
240+
llvm-cov report build-llvm/example_tests -instr-profile=build-llvm/default.profdata
241+
```
242+
243+
## Sanitizers
244+
245+
```cmake
246+
option(ENABLE_ASAN "Enable AddressSanitizer" OFF)
247+
option(ENABLE_UBSAN "Enable UndefinedBehaviorSanitizer" OFF)
248+
option(ENABLE_TSAN "Enable ThreadSanitizer" OFF)
249+
250+
if(ENABLE_ASAN)
251+
add_compile_options(-fsanitize=address -fno-omit-frame-pointer)
252+
add_link_options(-fsanitize=address)
253+
endif()
254+
if(ENABLE_UBSAN)
255+
add_compile_options(-fsanitize=undefined -fno-omit-frame-pointer)
256+
add_link_options(-fsanitize=undefined)
257+
endif()
258+
if(ENABLE_TSAN)
259+
add_compile_options(-fsanitize=thread)
260+
add_link_options(-fsanitize=thread)
261+
endif()
262+
```
263+
264+
## Flaky Tests Guardrails
265+
266+
- Never use `sleep` for synchronization; use condition variables or latches.
267+
- Make temp directories unique per test and always clean them.
268+
- Avoid real time, network, or filesystem dependencies in unit tests.
269+
- Use deterministic seeds for randomized inputs.
270+
271+
## Best Practices
272+
273+
### DO
274+
275+
- Keep tests deterministic and isolated
276+
- Prefer dependency injection over globals
277+
- Use `ASSERT_*` for preconditions, `EXPECT_*` for multiple checks
278+
- Separate unit vs integration tests in CTest labels or directories
279+
- Run sanitizers in CI for memory and race detection
280+
281+
### DON'T
282+
283+
- Don't depend on real time or network in unit tests
284+
- Don't use sleeps as synchronization when a condition variable can be used
285+
- Don't over-mock simple value objects
286+
- Don't use brittle string matching for non-critical logs
287+
288+
### Common Pitfalls
289+
290+
- **Using fixed temp paths** → Generate unique temp directories per test and clean them.
291+
- **Relying on wall clock time** → Inject a clock or use fake time sources.
292+
- **Flaky concurrency tests** → Use condition variables/latches and bounded waits.
293+
- **Hidden global state** → Reset global state in fixtures or remove globals.
294+
- **Over-mocking** → Prefer fakes for stateful behavior and only mock interactions.
295+
- **Missing sanitizer runs** → Add ASan/UBSan/TSan builds in CI.
296+
- **Coverage on debug-only builds** → Ensure coverage targets use consistent flags.
297+
298+
## Optional Appendix: Fuzzing / Property Testing
299+
300+
Only use if the project already supports LLVM/libFuzzer or a property-testing library.
301+
302+
- **libFuzzer**: best for pure functions with minimal I/O.
303+
- **RapidCheck**: property-based tests to validate invariants.
304+
305+
Minimal libFuzzer harness (pseudocode: replace ParseConfig):
306+
307+
```cpp
308+
#include <cstddef>
309+
#include <cstdint>
310+
#include <string>
311+
312+
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
313+
std::string input(reinterpret_cast<const char *>(data), size);
314+
// ParseConfig(input); // project function
315+
return 0;
316+
}
317+
```
318+
319+
## Alternatives to GoogleTest
320+
321+
- **Catch2**: header-only, expressive matchers
322+
- **doctest**: lightweight, minimal compile overhead

0 commit comments

Comments
 (0)