关于 coverage-analysis
coverage-analysis 是面向 QA 团队的测试覆盖率分析 Skill,帮助改进测试质量。它侧重分支与变异覆盖率、按风险分析缺口及 CI 阈值递增,而非只追求行覆盖数字。
主要功能
- 01分支与变异覆盖率指导
- 02按风险分析覆盖缺口
- 03CI 阈值与递增机制
- 04选择覆盖率工具
- 05合并与生成覆盖率报告
适用场景
- 01查找支付或认证逻辑的测试缺口
- 02阻止 CI 中覆盖率下降
- 03检查测试能否发现注入的缺陷
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ petrkindlmann/ qa-skills --skill coverage-analysis安装后在 Claude Code 中输入/coverage-analysis确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/ci-gating.mdmutation-testing.mdtool-config.md详细介绍
快速导航
| 情况 | 前往 |
|---|---|
| 选择并配置覆盖率工具 | Coverage Tools → references/tool-config.md |
| 决定接下来要为哪里编写测试 | Gap Analysis |
| 防止覆盖率在 CI 中回退 | Coverage as CI Gate → Ratchet Pattern |
| 向审查者展示每个 PR 的覆盖率 | Coverage as CI Gate → PR Diff |
| 测试运行了代码,却没有断言 | Mutation Testing |
| 决定排除哪些内容/设置什么目标 | Meaningful vs Vanity Coverage |
探索问题
先检查 .agents/qa-project-context.md——如果文件存在,请使用其中的信息,并跳过已回答的问题。然后:
- 配置了什么测试运行器和覆盖率工具? 检查
vitest.config.*(coverage 区块)、jest.config.*(coverageProvider)、.nycrc、脚本中的c8,或pyproject.toml中的[tool.coverage]。测试运行器决定要安装什么——Vitest 使用@vitest/coverage-v8,而不是 c8(参见 Coverage Tools)。 - 当前覆盖率是多少? 运行现有的覆盖率命令,记录行、分支和函数覆盖率百分比。这是棘轮机制的基准。
- CI 中是否设置了覆盖率门槛? 检查 GitHub Actions / GitLab CI 中是否有
--coverage、coverageThreshold、fail_under或--cov-fail-under。没有门槛意味着覆盖率只是装饰。 - 目标是什么,由谁设定? 没有依据的目标(“副总裁说要达到 80%”)会助长弄虚作假。目标应反映风险承受能力和代码库成熟度,而不是一个整数。
核心原则
1. 覆盖率衡量广度,而非深度。 执行了某一行代码,不代表它得到了正确测试。expect(true).toBe(true) 会执行函数,却没有断言任何内容。覆盖率能告诉你哪些代码运行过,却不能说明测试能否发现 bug——这正是变异测试要衡量的内容。
2. 分支覆盖率比行覆盖率更重要。 一行中的三元表达式 condition ? a : b,即使只运行了一个分支,在线覆盖率中也会被视为完全覆盖。行覆盖率不能保证分支覆盖率。应以分支覆盖率而不只是行覆盖率设定门槛:
function discount(price: number, isPremium: boolean): number {
return isPremium ? price * 0.8 : price;
}
// Line coverage: 100% (the line executed). Branch coverage: 50% (only the true branch ran).
expect(discount(100, true)).toBe(80);
// Fix — assert BOTH paths so branch coverage reaches 100%:
expect(discount(100, true)).toBe(80);
expect(discount(100, false)).toBe(100);
3. 棘轮机制:绝不降低,只能提高。 将当前覆盖率设为最低门槛。每个 PR 都必须达到或超过该门槛。覆盖率会随时间稳步提高,无需预先设定人为目标。
4. 按风险关注缺口,而不是数字。 覆盖率达到 85% 的项目并不一定比达到 75% 的项目“更好”。关键在于未测试的部分是否包含支付、身份验证或数据完整性逻辑。应按风险分析缺口。
5. 新代码的标准应高于旧代码。 即使整个项目的覆盖率只有 65%,也要要求 PR 中的新代码达到 90% 以上。这样既能防止覆盖率下滑,又不必强制重写旧代码。
覆盖率工具
先按测试运行器选择提供程序,再看它与构建输出的匹配程度。
| 运行器 / 上下文 | 安装 | 提供程序 |
|---|---|---|
| Vitest | @vitest/coverage-v8(默认)或 @vitest/coverage-istanbul | coverage.provider: 'v8' / 'istanbul' |
| Jest | 内置(coverageProvider: 'v8' 或 'babel') | V8 或 Istanbul/babel |
非 Vitest Node(node:test、纯 mocha) | c8 CLI | 通过 c8 <command> 使用 V8 |
| 旧版 Istanbul CLI | nyc | Istanbul 插桩 |
| Python | pytest-cov(封装 coverage.py) | coverage.py |
底层有两种引擎:
- V8 覆盖率 — 内置于 V8 引擎,因此不会对源代码进行插桩:速度更快,也无需 Babel 转换。对于 Vitest,请安装
@vitest/coverage-v8(不要安装c8——它是供非 Vitest 运行器使用的独立 CLI)。对于纯node:test或 mocha 项目,c8是封装相同 V8 数据的 CLI。新建 Node/Vitest 项目的默认选择。 - Istanbul — 对源代码进行插桩;速度较慢,但通过转译器和打包器的映射更可靠。V8 映射效果不佳时,切换到 Istanbul(
@vitest/coverage-istanbul或nyc)。V8 映射效果不佳的表现: 报告中的未覆盖行落在空行、右花括号或装饰器上,或者整个已覆盖函数都显示为红色——这意味着源映射错误地归属了行(某些 TS 打包器 / SWC 配置中很常见)。遇到这种情况时,切换到 Istanbul 提供程序。
Node 版本基线: 当前版本为
c811.x 和nyc18.x。c8 11 仍支持 Node >=12;nyc 18 要求 Node 20 || >= 22。如果必须使用 Node 18,请固定为nyc@^17(c8 11 在 Node 18 上运行正常)。新项目应统一使用 Node 20+。
完整的提供程序配置(Vitest coverage 配置块、.nycrc.json、Jest coverageThreshold、pyproject.toml / .coveragerc.toml)、安装命令和运行命令,请参阅 references/tool-config.md。
合并不同测试类型的覆盖率
单元测试、集成测试和 E2E 测试运行都会生成部分覆盖率数据。将它们合并,避免仅由集成测试覆盖的行被报告为缺口:
- Vitest — 以多个项目 / 配置运行并让 Vitest 合并,或合并
coverage-final.json输出。 - nyc —
nyc merge .nyc_output merged.json && nyc report -t merged会合并多次单独运行生成的.json文件。 - coverage.py — 使用
coverage run -p运行每个测试套件后,执行coverage combine。
先合并,再根据合并后的总覆盖率设置门槛。不要单独对每个测试套件的覆盖率设置门槛。
覆盖率报告类型
| 报告器 | 输出 | 使用场景 |
|---|---|---|
text | 终端表格 | 快速本地检查 |
html | 交互式 HTML | 详细的本地分析,可逐个查看文件 |
lcov | lcov.info 文件 | SonarQube、Codecov、Coveralls 集成 |
json-summary | coverage-summary.json | CI 脚本、PR 评论、仪表板指标 |
cobertura | cobertura-coverage.xml | GitLab CI 覆盖率可视化 |
差距分析
覆盖率报告会显示哪些代码行和分支未运行。差距的重要性各不相同,应按风险确定优先级。
步骤 1:生成报告。
npm run test:coverage
# Open coverage/index.html in a browser
步骤 2:按未覆盖行数对文件排序。 解析 coverage-summary.json,按 (total - covered) 降序对文件排序,重点关注排名前 20 的文件。编写一个读取 JSON 摘要并输出文件、行覆盖率、分支覆盖率和未覆盖数量的小脚本,可让这一流程可重复执行。
步骤 3:根据风险对差距分类。
| 差距位置 | 风险级别 | 操作 |
|---|---|---|
| 支付处理 | 严重 | 立即编写测试 |
| 身份验证/权限 | 严重 | 立即编写测试 |
| 数据验证 | 高 | 列入下一个冲刺 |
| 错误处理路径 | 高 | 列入下一个冲刺 |
| 实用函数 | 中 | 修改时补充覆盖 |
| UI 格式设置 | 低 | 除非容易引发回归,否则跳过 |
| 生成的代码 | 无 | 从覆盖率统计中排除 |
排序时应纳入分支覆盖率,而不只是代码行数——某个文件行覆盖率为 100%、分支覆盖率为 50%,意味着仍有未测试的路径;仅按代码行排序时,它会被错误地列为“已完成”。
将覆盖率设为 CI 门禁
阈值配置
将全局阈值设为项目的最低要求,再为关键代码(支付、身份验证)设置比实用工具更严格的目录级阈值。Vitest 在 thresholds 下使用 glob 键(例如 "src/payments/**": { lines: 95, branches: 90 });Jest 在 coverageThreshold 下使用路径键。两者都支持按路径覆盖配置。
全局、目录级和 Jest 单文件阈值的配置请参阅 references/ci-gating.md。
阈值递增模式
绝不允许覆盖率下降。将当前水平设为最低要求,并在覆盖率提升时逐步提高下限。递增脚本读取 coverage-summary.json,将各项指标与已提交的 .coverage-ratchet.json 比较,出现任何退步时都会使构建失败,并在覆盖率提升时更新基线。
提交 .coverage-ratchet.json(例如 { "lines": 82, "branches": 78, ... })。在 CI 中,于测试后运行递增脚本。在主分支合并时,如果覆盖率有所提升,则自动提交更新后的递增文件。
完整的 coverage-ratchet.ts 脚本请参阅 references/ci-gating.md。
PR 差异覆盖率门禁
要求 PR 中的新代码达到高于项目基线的阈值(例如 90%)。在 CI 中,使用 git diff --name-only origin/main...HEAD 找出已更改的文件,然后从 coverage-summary.json 中检查这些文件的覆盖率。如果已更改文件的覆盖率低于阈值,则让流水线失败。这样无需重写旧代码,也能阻止覆盖率持续下降。
使用 davelosert/vitest-coverage-report-action@v2(读取 JSON 摘要),或配合筛选已更改文件的脚本使用 marocchino/sticky-pull-request-comment@v2,向审查者展示差异。完整的 PR 工作流请参阅 references/ci-gating.md。
托管服务替代方案: Codecov、Coveralls 和 Trunk Coverage 均提供一流的 PR 差异覆盖率功能,包括阻止合并的门禁和行内注释。大多数团队更倾向于使用这些服务,而不是自行编写差异脚本——如果你还没有覆盖率托管服务,可以选择其中之一。Codecov + GitHub:codecov/codecov-action@v5 会读取 lcov.info 并自动发布 PR 差异评论。
变异测试
变异测试衡量的是断言质量,而不只是代码是否执行。它会修改源代码(将 > 翻转为 >=、删除一行),然后检查测试是否失败。存活的变异体意味着存在测试无法发现的真实缺陷。借助 Stryker JS v9.6+ 和 Vitest 4.1+,在 PR 更改的文件上运行变异测试的成本足够低;mutmut 3.x 则支持 Python。
目标范围
对整个代码库进行变异测试成本很高——应当增量运行。Stryker 的 incremental: true(JSON 缓存)配合将 --mutate 限定为 git diff 中的文件,只会重新对改动过的文件进行变异;mutmut 同样会按路径进行变异。范围应限定在:
- 纯业务逻辑(验证器、计算器、转换器)
- 关键路径(支付、身份验证、数据完整性)
- 行覆盖率高但断言可疑的代码(分支覆盖率 > 90%,但断言变体很少)
跳过 UI 渲染、胶水代码和生成代码。
参见 references/mutation-testing.md,了解 Stryker 配置(stryker.config.json、增量运行、仅对已更改的文件运行)和 mutmut 调用方式。
解读分数
变异分数为 80%,意味着注入的 bug 中有 80% 被捕获。低于覆盖率百分比是正常的——许多变异体会落在未测试的分支中,而覆盖率报告已经标出了这些分支。值得关注的信号是高覆盖率 + 低变异分数:代码执行了,但断言没有对其加以约束。
有意义的覆盖率与虚高的覆盖率
为什么 100% 覆盖率通常是错误的目标
达到 100% 需要测试每一行的每个分支,包括:
- 不可能状态的错误处理
- 穷尽式 switch 语句中的默认分支
- 从未被直接调用的框架生命周期方法
- 防御损坏数据的检查
为达到 100% 而编写的测试往往流于形式、脆弱,而且无法捕获真正的 bug。
边际收益递减
| 覆盖率范围 | 价值 | 工作量 |
|---|---|---|
| 0% 到 60% | 高——主要路径、明显的回归问题 | 低 |
| 60% 到 80% | 中——错误路径、边界情况 | 中 |
| 80% 到 90% | 较低——特殊组合、防御性代码 | 高 |
| 90% 到 100% | 极低——不可达代码、框架内部实现 | 很高 |
大多数项目的最佳范围是 75–85%。关键路径(支付、身份验证)应设定更高目标(90%+)。将全局阈值设在最佳范围内,并为支付/身份验证设置 90%+ 的目录级阈值。
不要覆盖的内容
排除以下内容——它们会扩大分母,却没有增加价值。请在 CONTRIBUTING/覆盖率说明中记录每项排除的理由,以免排除列表悄悄掩盖真正的缺口。
// vitest.config.ts / jest.config.js — exclude patterns
exclude: [
"**/*.d.ts", // Type definitions
"**/index.ts", // Barrel exports (re-exports only)
"**/*.stories.{ts,tsx}", // Storybook stories
"**/generated/**", // Auto-generated code (GraphQL, Prisma)
"**/migrations/**", // Database migrations
"**/__mocks__/**", // Test mocks
"**/types/**", // Type-only modules
]
百分比之外的质量指标
| 指标 | 衡量内容 | 获取方式 |
|---|---|---|
| 变异分数 | 测试能否捕获真正的 bug? | Stryker / mutmut |
| 分支覆盖率 | 所有条件路径是否都经过测试? | 使用 V8/Istanbul 并启用分支报告 |
| 关键路径覆盖率 | 支付/身份验证/数据流是否都得到完整覆盖? | 目录级阈值 |
| 缺陷逃逸率 | 生产环境 bug 是否出现在已测试代码中? | 事后事件分析 |
| 覆盖率变化 | 覆盖率是在提升还是下降? | 跟踪棘轮模式 |
反模式
1. 将覆盖率视为质量证明
“我们的覆盖率有 90%,所以测试很充分”这种想法很危险。覆盖率说明代码执行过,并不代表行为经过了验证。**改进方法:**将关键模块的变异分数与覆盖率百分比配合使用;即使行覆盖率达到 100%,没有断言的测试也应拉低变异分数。
2. 通过排除文件抬高数字
将难以测试的文件(错误处理器、集成模块)加入排除列表,会掩盖最重要的缺口。**改进方法:**只排除确实无法测试的代码——生成文件、类型定义、barrel 导出——并书面说明每项排除的理由。
3. 为达标而编写流于形式的测试
it("should exist", () => expect(MyClass).toBeDefined()) 虽然增加了覆盖率,却没有价值。**改进方法:**每个测试都应验证一种行为;如果该行为出错,就会影响用户。变异测试可以标记这类无效测试。
4. 使用全局阈值,却不分析各模块
即使支付模块的覆盖率只有 30%,只要工具类拉高了平均值,80% 的全局阈值仍会通过。**改进方法:**为支付/身份验证设置 90%+ 的目录级阈值。
5. 覆盖率阈值设定后从不调整
团队连续 6 个月停滞在 78%,说明没有进步。**修复方法:**自动逐步提高最低阈值;每季度审查一次,并调查停滞原因。
6. 忽略分支覆盖率
即使某个分支从未执行,const r = cond ? a : b 的行覆盖率报告仍会显示 100%。**修复方法:**始终同时报告 branches 和 lines,并将两者都设为门槛。
7. 只统计 E2E 测试的覆盖率
单个 E2E 测试触及了 60% 的代码库,却没有测试任何边界情况——覆盖面广,但测试浅。**修复方法:**分别衡量单元测试/集成测试覆盖率和 E2E 覆盖率,并以单元测试/集成测试的总覆盖率作为门槛;E2E 覆盖率只是额外参考指标,而非门槛。
验证
证明门槛确实会在回归时触发——没有强制执行的绿色构建毫无价值。
- 阈值会触发:暂时将一个已提交的指标降到当前值以下(或删除一个通过的测试),运行
npm run test:coverage(或pytest --cov=src --cov-fail-under=80),并确认退出代码非零。之后恢复。 - **自动递增机制会触发:**提交
.coverage-ratchet.json后,删除一个测试以造成覆盖率回退,运行自动递增脚本,并确认它输出FAIL: ... coverage dropped且以 1 退出。 - **已启用分支门槛:**确认报告中显示
branches列,且阈值配置包含branches,而不只是lines。 - **PR 差异可正常显示:**创建一个涉及某个源文件的草稿 PR,并确认覆盖率差异评论已发布,且包含变更文件的数字。
如果降低覆盖率后,第 1 步仍以 0 退出,说明门槛尚未接入——在宣称完成之前先修复。
完成标准
- CI 会在每次推送时自动运行覆盖率检查(无需手动生成报告)。
- CI 会强制执行覆盖率阈值:当行覆盖率或分支覆盖率低于定义的最低值时,构建以非零代码退出(按「验证」第 1 步进行确认)。
- 覆盖率报告会作为 CI 工件发布(HTML +
json-summary),并在 PR 评论中发布每个 PR 的覆盖率变化。 - 覆盖率配置包含排除列表和按目录设置的阈值(payments/auth 为 90% 以上),且 CONTRIBUTING/coverage 文档列明了每项排除的理由。
- 自动递增机制已接入:已提交
.coverage-ratchet.json,自动递增脚本会在 CI 中运行,且构建会在相对记录基线出现回归时失败(按「验证」第 2 步进行确认)。
参考文件(位于 references/)
- tool-config.md — Vitest(
@vitest/coverage-v8/-istanbul)、用于非 Vitest 测试运行器的 c8 CLI、nyc、Jest 和 coverage.py 的完整配置,以及安装和运行命令。 - ci-gating.md — PR 覆盖率差异工作流、全局/按目录/按文件设置的阈值,以及
coverage-ratchet.ts脚本。 - mutation-testing.md — 用于衡量断言质量的 Stryker(
stryker.config.json,增量模式)和 mutmut 配置。
相关技能
- unit-testing — 编写能提升覆盖率的测试:模拟策略、框架专属配置,以及用于在 CI 中仅统计变更文件覆盖率的 Vitest
coverage.changed。前往该技能编写测试;此技能负责衡量并为测试设置门槛。 - ci-cd-integration — 覆盖率门槛、工件存储和 PR 评论的流水线接入。
- qa-metrics — 将覆盖率与变异测试得分、缺陷逃逸率一起作为持续跟踪的 KPI 趋势指标。前往该技能查看仪表板和趋势;此技能负责单个仓库的配置和门槛设置。
- ai-qa-review — 借助 AI 识别测试不足的路径,以及使用 Vitest 浏览器模式确保组件覆盖率一致。