27、测试覆盖率门禁:设置覆盖率阈值、CI中自动检查、增量覆盖率分析

覆盖率门禁,说白了就是给代码质量设一道红线。我见过太多项目,一开始大家热情高涨,覆盖率冲到80%。三个月后呢?掉到30%了。没人管,也没人敢管。

为什么?因为没有门禁。今天我们就来聊聊怎么把这扇门立起来。

覆盖率阈值:定多少才合理?

先说说阈值怎么设。我个人习惯分三层:

  • 核心模块:80%以上。比如支付、鉴权、数据持久化。
  • 业务模块:60%以上。比如订单处理、用户管理。
  • 工具/辅助模块:40%以上。比如日志、配置读取。

你可能会问:为什么不是统一90%?我在项目中遇到过,团队为了达标,写了一堆「假测试」——只调用函数但不做断言。覆盖率数字好看,实际屁用没有。

核心原则:覆盖率阈值不是越高越好,而是「能拦住明显问题」就好。剩下的靠代码审查和集成测试补。

具体怎么在CMake里设?用 --coverage 编译选项,配合 lcov 生成报告,然后写个脚本检查百分比。我一般这样写:

# CMakeLists.txt 中启用覆盖率
if(CMAKE_BUILD_TYPE STREQUAL "Coverage")
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --coverage -O0 -g")
    set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --coverage")
endif()

然后在CI脚本里跑:

# 生成覆盖率报告
lcov --capture --directory . --output-file coverage.info
lcov --remove coverage.info '/usr/*' '*/test/*' --output-file filtered.info
# 提取行覆盖率
LINE_COV=$(lcov --summary filtered.info | grep "lines" | awk '{print $2}' | cut -d'%' -f1)
echo "Line coverage: $LINE_COV%"
# 检查阈值
if (( $(echo "$LINE_COV < 60" | bc -l) )); then
    echo "❌ 覆盖率未达标!"
    exit 1
fi

小技巧:用 bc 做浮点数比较,别用 if [ $LINE_COV -lt 60 ],那只能比较整数。

CI中自动检查:让门禁跑起来

阈值设好了,怎么让它自动执行?嗯,这里要注意:门禁必须跑在CI里,不能靠人工。人工检查覆盖率?我试过,两周后就没人记得了。

我建议用GitLab CI或GitHub Actions。以GitHub Actions为例:

# .github/workflows/coverage.yml
name: Coverage Gate
on: [pull_request]
jobs:
  coverage:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Build with coverage
        run: |
          cmake -B build -DCMAKE_BUILD_TYPE=Coverage
          cmake --build build
      - name: Run tests
        run: ctest --test-dir build
      - name: Check coverage
        run: |
          lcov --capture --directory build --output-file coverage.info
          lcov --remove coverage.info '/usr/*' '*/test/*' --output-file filtered.info
          LINE_COV=$(lcov --summary filtered.info | grep "lines" | awk '{print $2}' | cut -d'%' -f1)
          if (( $(echo "$LINE_COV < 60" | bc -l) )); then
            echo "❌ 覆盖率 $LINE_COV% 低于阈值 60%"
            exit 1
          fi
          echo "✅ 覆盖率 $LINE_COV% 通过"

你看,整个流程就是:编译→测试→生成报告→检查阈值→通过或失败。一旦失败,PR直接打回,开发者必须补测试。

注意:别把门禁设得太死。我见过一个团队,阈值设了85%,结果每次PR都要花半天补测试。后来降到70%,反而更健康。门禁是工具,不是枷锁。

增量覆盖率分析:只盯新代码

全量覆盖率有个大问题:老代码已经覆盖了,新代码没覆盖,但整体数字还是好看。比如一个项目全量覆盖率70%,你加了100行新代码,一行测试没写,全量覆盖率可能只掉到69.5%。根本看不出来。

所以,我强烈建议做增量覆盖率分析。说白了,只检查本次PR新增的代码,覆盖率够不够。

怎么做?用 diff 工具配合 gcov。我常用的脚本:

# 获取本次PR修改的文件
git diff --name-only origin/main...HEAD | grep '\.cpp$' > changed_files.txt

# 对每个文件检查覆盖率
while IFS= read -r file; do
    # 提取新增行号
    git diff origin/main...HEAD -- "$file" | grep '^+' | grep -v '^+++' | wc -l
    # 用gcov看哪些行没覆盖
    gcov "$file" -o build/CMakeFiles/...
    # 计算新增行中未覆盖的比例
    # ...(具体实现略长,这里展示思路)
done < changed_files.txt

更省事的办法是用现成工具,比如 diff-cover

pip install diff-cover
diff-cover coverage.xml --compare-branch=origin/main --fail-under=80

这个工具会告诉你:新增代码中,哪些行没被测试覆盖到。我习惯设增量阈值80%——新代码必须覆盖80%以上,否则门禁不通过。

为什么是80%? 因为新代码通常逻辑集中,容易写测试。如果新代码都覆盖不到80%,说明要么测试太难写(设计问题),要么开发者偷懒了。

知识体系图

下面这张图总结了覆盖率门禁的核心逻辑:

覆盖率门禁核心流程 PR / 代码提交 CMake 构建 + CTest 运行测试 lcov / gcov 生成覆盖率报告 全量覆盖率 ≥ 阈值? 增量覆盖率 ≥ 阈值? ❌ 拒绝合并 ✅ 允许合并 (两者都通过才允许合并)

避坑指南

最后,分享几个我踩过的坑:

  • 别用行覆盖率当唯一标准。行覆盖率100%不代表分支都覆盖了。我建议同时看分支覆盖率(branch coverage)。
  • 增量分析要排除测试文件本身。测试文件的覆盖率没意义,你想想看,测试代码自己不需要被测试。
  • 门禁失败时,给开发者明确的反馈。别只抛个「覆盖率未达标」,要告诉具体哪个文件、哪几行没覆盖。我习惯在CI日志里输出:❌ src/payment.cpp:45-67 未覆盖
  • 定期调整阈值。项目初期可以设低点(40%),稳定后逐步提高到60%、80%。别一上来就定死。

我的习惯:每次迭代结束后,我会看一次全量覆盖率趋势。如果连续两个迭代下降,就组织一次「测试补全日」,专门补老代码的测试。门禁管增量,人工管存量,双管齐下。

覆盖率门禁不是银弹,但它能帮你守住底线。尤其是团队大了以后,没有门禁,覆盖率就是一句空话。从今天开始,给你的项目加一扇门吧。


公众号:蓝海资料掘金营,微信deep3321