Skip to content

单元测试设计与持续集成实践

GoogleTest 的最终目标是服务工程质量。一个项目是否真正用好了单元测试,要看它能否在本地和 CI 中快速发现回归,并让开发者愿意持续维护。

一个好的单元测试

特征 说明
快速 本地频繁运行不造成负担
稳定 不依赖测试顺序、系统时间、网络波动
聚焦 一个测试验证一个主要行为
可读 失败后从名字和断言能看懂问题
可维护 不因为内部实现重构而大量失败

Arrange Act Assert

TEST(PasswordPolicyTest, RejectsPasswordShorterThanMinimumLength) {
    PasswordPolicy policy{/*minimum_length=*/8};

    const bool accepted = policy.Accept("abc");

    EXPECT_FALSE(accepted);
}

这个结构让测试具有明确节奏:先准备,再执行,最后断言。

测试边界

对一个函数或类,至少考虑以下输入:

  • 最小值、最大值、空值。
  • 正常路径和错误路径。
  • 重复调用后的状态变化。
  • 资源不足或依赖失败。
  • 历史 bug 对应的回归样例。

避免脆弱测试

脆弱测试通常过度绑定实现细节。例如测试一个排序函数时,应该验证输出有序且元素集合不变,而不是验证内部使用了快速排序还是归并排序。

TEST(SortTest, SortsValuesAscending) {
    std::vector<int> values = {3, 1, 2};

    SortAscending(values);

    EXPECT_THAT(values, ::testing::ElementsAre(1, 2, 3));
}

处理外部依赖

依赖 建议
时间 注入 Clock 接口或传入时间点
随机数 注入随机数生成器或固定种子
文件系统 使用临时目录,测试后清理
网络 使用本地假服务或接口替身
数据库 使用测试库、事务回滚或内存实现

CI 中的测试步骤

拉取代码
  -> 配置 CMake
  -> 构建
  -> 运行 CTest
  -> 生成覆盖率
  -> 上传测试报告和覆盖率报告

示例命令:

cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
cmake --build build --parallel
ctest --test-dir build --output-on-failure

覆盖率构建建议独立:

cmake -S . -B build-coverage -DCMAKE_BUILD_TYPE=Debug -DENABLE_COVERAGE=ON
cmake --build build-coverage --parallel
ctest --test-dir build-coverage --output-on-failure
gcovr -r . build-coverage --xml-pretty -o coverage.xml --print-summary

覆盖率门禁

覆盖率门禁适合防止测试大幅退化,但阈值要循序渐进。

gcovr -r . build-coverage \
    --fail-under-line 80 \
    --fail-under-branch 70

不要一开始就给历史项目设置过高阈值。更务实的做法是先记录基线,再要求新增代码不降低覆盖率。

测试审查清单

  • 测试名是否表达行为和条件。
  • 失败信息是否足够定位问题。
  • 是否覆盖边界值和错误路径。
  • 是否存在测试顺序依赖。
  • 是否访问了真实外部环境。
  • 是否把实现细节写死在断言里。
  • 是否能被 CTest 或 CI 自动运行。

何时不适合单元测试

不是所有验证都应该塞进 GoogleTest 单元测试。

场景 更合适方式
UI 交互 UI 自动化或端到端测试
性能基准 Benchmark 工具
编译期接口约束 static_assert 或编译测试
大规模系统联调 集成测试环境
内存错误检测 ASan、UBSan、Valgrind 等工具

总结

GoogleTest 是测试表达工具,工程质量来自良好的测试边界、稳定的依赖控制、清晰的构建入口和持续运行的反馈机制。先让测试可靠,再追求覆盖率;先让失败可定位,再追求测试数量。