C++ 测试(代理技能)
针对现代 C++(C++17/20)的代理导向测试工作流,使用 GoogleTest/GoogleMock 和 CMake/CTest。
使用时机
- 编写新的 C++ 测试或修复现有测试
- 为 C++ 组件设计单元/集成测试覆盖
- 添加测试覆盖、CI 门控或回归保护
- 配置 CMake/CTest 工作流以实现一致的执行
- 调查测试失败或偶发性行为
- 启用用于内存/竞态诊断的消毒剂
不适用时机
- 在不修改测试的情况下实现新的产品功能
- 与测试覆盖或失败无关的大规模重构
- 没有测试回归需要验证的性能调优
- 非 C++ 项目或非测试任务
核心概念
- TDD 循环:红 → 绿 → 重构(先写测试,最小化修复,然后清理)。
- 隔离:优先使用依赖注入和仿制品,而非全局状态。
- 测试布局:
tests/unit、tests/integration、tests/testdata。 - Mock 与 Fake:Mock 用于交互,Fake 用于有状态行为。
- CTest 发现:使用
gtest_discover_tests()进行稳定的测试发现。 - CI 信号:先运行子集,然后使用
--output-on-failure运行完整套件。
TDD 工作流
遵循 RED → GREEN → REFACTOR 循环:
- RED:编写一个捕获新行为的失败测试
- GREEN:实现最小的更改以使其通过
- REFACTOR:在测试保持通过的同时进行清理
代码示例
基础单元测试 (gtest)
夹具 (gtest)
Mock (gmock)
CMake/CTest 快速入门
运行测试
调试失败
- 使用 gtest 过滤器重新运行单个失败的测试。
- 在失败的断言周围添加作用域日志记录。
- 启用消毒剂后重新运行。
- 根本原因修复后,扩展到完整套件。
覆盖率
优先使用目标级别的设置,而非全局标志。
GCC + gcov + lcov:
Clang + llvm-cov:
消毒剂
偶发性测试防护
- 切勿使用
sleep进行同步;使用条件变量或门闩。 - 为每个测试创建唯一的临时目录并始终清理它们。
- 避免在单元测试中依赖真实时间、网络或文件系统。
- 对随机化输入使用确定性种子。
最佳实践
应该做
- 保持测试的确定性和隔离性
- 优先使用依赖注入而非全局变量
- 对前置条件使用
ASSERT_*,对多个检查使用EXPECT_* - 在 CTest 标签或目录中分离单元测试与集成测试
- 在 CI 中运行消毒剂以进行内存和竞态检测
不应该做
- 不要在单元测试中依赖真实时间或网络
- 当可以使用条件变量时,不要使用睡眠作为同步手段
- 不要过度模拟简单的值对象
- 不要对非关键日志使用脆弱的字符串匹配
常见陷阱
- 使用固定的临时路径 → 为每个测试生成唯一的临时目录并清理它们。
- 依赖挂钟时间 → 注入时钟或使用模拟时间源。
- 偶发性并发测试 → 使用条件变量/门闩和有界等待。
- 隐藏的全局状态 → 在夹具中重置全局状态或移除全局变量。
- 过度模拟 → 对有状态行为优先使用 Fake,仅对交互进行 Mock。
- 缺少消毒剂运行 → 在 CI 中添加 ASan/UBSan/TSan 构建。
- 仅在调试版本上计算覆盖率 → 确保覆盖率目标使用一致的标志。
可选附录:模糊测试 / 属性测试
仅在项目已支持 LLVM/libFuzzer 或属性测试库时使用。
- libFuzzer:最适合 I/O 最少的纯函数。
- RapidCheck:基于属性的测试,用于验证不变量。
最小的 libFuzzer 测试框架(伪代码:替换 ParseConfig):
GoogleTest 的替代方案
- Catch2:仅头文件,表达性强的匹配器
- doctest:轻量级,编译开销最小


