mcpField
C
coverage-analysis

coverage-analysis

github.com/petrkindlmann测试与质量2291.1k 次安装1 周前更新
代码仓库

关于 coverage-analysis

coverage-analysis 是面向 QA 团队的测试覆盖率分析 Skill,帮助改进测试质量。它侧重分支与变异覆盖率、按风险分析缺口及 CI 阈值递增,而非只追求行覆盖数字。

主要功能

  1. 01分支与变异覆盖率指导
  2. 02按风险分析覆盖缺口
  3. 03CI 阈值与递增机制
  4. 04选择覆盖率工具
  5. 05合并与生成覆盖率报告

适用场景

  1. 01查找支付或认证逻辑的测试缺口
  2. 02阻止 CI 中覆盖率下降
  3. 03检查测试能否发现注入的缺陷

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/petrkindlmann/qa-skills --skill coverage-analysis

安装后在 Claude Code 中输入/coverage-analysis确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
ci-gating.md
mutation-testing.md
tool-config.md

详细介绍

一个行覆盖率达到 90%、但测试中没有任何断言的测试套件,一个 bug 都抓不到——行覆盖率只能证明代码运行过,不能证明回归问题会被发现。此技能会衡量真正重要的指标(分支覆盖率、变异得分、关键路径覆盖率),通过棘轮机制在 CI 中设置门槛,确保覆盖率只能提高,并按风险而不是追逐虚荣数字来揭示缺口。它可以避免一种典型失败:覆盖率徽章显示绿色,测试套件却从未做出有意义的断言,而支付模块的覆盖率只有 30%。

快速导航

情况前往
选择并配置覆盖率工具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——如果文件存在,请使用其中的信息,并跳过已回答的问题。然后:

  1. 配置了什么测试运行器和覆盖率工具? 检查 vitest.config.*(coverage 区块)、jest.config.*(coverageProvider)、.nycrc、脚本中的 c8,或 pyproject.toml 中的 [tool.coverage]。测试运行器决定要安装什么——Vitest 使用 @vitest/coverage-v8,而不是 c8(参见 Coverage Tools)。
  2. 当前覆盖率是多少? 运行现有的覆盖率命令,记录行、分支和函数覆盖率百分比。这是棘轮机制的基准。
  3. CI 中是否设置了覆盖率门槛? 检查 GitHub Actions / GitLab CI 中是否有 --coverage、coverageThreshold、fail_under 或 --cov-fail-under。没有门槛意味着覆盖率只是装饰。
  4. 目标是什么,由谁设定? 没有依据的目标(“副总裁说要达到 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-istanbulcoverage.provider: 'v8' / 'istanbul'
Jest内置(coverageProvider: 'v8' 或 'babel')V8 或 Istanbul/babel
非 Vitest Node(node:test、纯 mocha)c8 CLI通过 c8 <command> 使用 V8
旧版 Istanbul CLInycIstanbul 插桩
Pythonpytest-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 版本基线: 当前版本为 c8 11.x 和 nyc 18.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详细的本地分析,可逐个查看文件
lcovlcov.info 文件SonarQube、Codecov、Coveralls 集成
json-summarycoverage-summary.jsonCI 脚本、PR 评论、仪表板指标
coberturacobertura-coverage.xmlGitLab 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 覆盖率只是额外参考指标,而非门槛。


验证

证明门槛确实会在回归时触发——没有强制执行的绿色构建毫无价值。

  1. 阈值会触发:暂时将一个已提交的指标降到当前值以下(或删除一个通过的测试),运行 npm run test:coverage(或 pytest --cov=src --cov-fail-under=80),并确认退出代码非零。之后恢复。
  2. **自动递增机制会触发:**提交 .coverage-ratchet.json 后,删除一个测试以造成覆盖率回退,运行自动递增脚本,并确认它输出 FAIL: ... coverage dropped 且以 1 退出。
  3. **已启用分支门槛:**确认报告中显示 branches 列,且阈值配置包含 branches,而不只是 lines。
  4. **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 浏览器模式确保组件覆盖率一致。

评论

支持 Markdown