coverage-analysis
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chinese<objective>
A suite at 90% line coverage with assertion-free tests catches zero bugs — line coverage
proves code ran, not that a regression would be caught. This skill measures the right
things (branch coverage, mutation score, critical-path coverage), gates them in CI with a
ratchet so coverage can only go up, and surfaces gaps by risk instead of chasing a vanity
number. It prevents the classic failure: a green coverage badge over a test suite that
never asserts anything meaningful, while the payment module sits at 30%.
</objective>
<目标>
一套达到90%行覆盖率但无断言测试的测试套件无法发现任何bug——行覆盖率只能证明代码被执行过,无法证明能捕获回归问题。本技能聚焦于衡量正确的指标(分支覆盖率、突变分数、关键路径覆盖率),在CI中通过棘轮机制确保覆盖率只升不降,并按风险等级呈现覆盖率缺口,而非盲目追逐表面数字。它能避免典型的失效场景:测试套件显示绿色覆盖率徽章,但从未做出任何有意义的断言,而支付模块的覆盖率仅为30%。
</目标>
Quick Route
快速指引
| Situation | Go to |
|---|---|
| Pick and configure a coverage provider | Coverage Tools → |
| Decide where to write tests next | Gap Analysis |
| Stop coverage from regressing in CI | Coverage as CI Gate → Ratchet Pattern |
| Show per-PR coverage to reviewers | Coverage as CI Gate → PR Diff |
| Tests run code but don't assert | Mutation Testing |
| Decide what to exclude / what target to set | Meaningful vs Vanity Coverage |
| 场景 | 参考内容 |
|---|---|
| 选择并配置覆盖率工具 | Coverage Tools → |
| 确定下一步测试编写方向 | 覆盖率缺口分析 |
| 防止CI中覆盖率倒退 | CI覆盖率门禁 → 棘轮模式 |
| 向评审者展示PR级覆盖率 | CI覆盖率门禁 → PR差异检查 |
| 测试执行代码但未添加断言 | 突变测试 |
| 确定排除范围/设置覆盖率目标 | 有意义vs表面覆盖率 |
Discovery Questions
调研问题
Check first — if it exists, use it and skip anything already answered there. Then:
.agents/qa-project-context.md- What test runner and coverage tooling is configured? Check for (coverage block),
vitest.config.*(coverageProvider),jest.config.*,.nycrcin scripts, orc8in[tool.coverage]. The runner decides the install — Vitest pullspyproject.toml, not c8 (see Coverage Tools).@vitest/coverage-v8 - What is the current coverage level? Run the existing coverage command and note line, branch, and function percentages. This is the baseline for the ratchet.
- Is coverage gated in CI? Check GitHub Actions / GitLab CI for ,
--coverage,coverageThreshold, orfail_under. No gate means coverage is decorative.--cov-fail-under - What is the target, and who set it? A target without rationale ("the VP said 80%") leads to gaming. Targets should reflect risk tolerance and codebase maturity, not a round number.
先查看 ——如果存在,使用其中内容并跳过已回答的问题。然后:
.agents/qa-project-context.md- 当前配置了什么测试运行器和覆盖率工具? 检查是否存在 (coverage配置块)、
vitest.config.*(coverageProvider)、jest.config.*、脚本中的.nycrc,或c8中的pyproject.toml。测试运行器决定了安装方式——Vitest需安装[tool.coverage],而非c8(详见覆盖率工具章节)。@vitest/coverage-v8 - 当前覆盖率水平是多少? 运行现有覆盖率命令,记录行、分支和函数覆盖率百分比。这将作为棘轮机制的基准线。
- CI中是否设置了覆盖率门禁? 检查GitHub Actions/GitLab CI中是否存在、
--coverage、coverageThreshold或fail_under。无门禁意味着覆盖率仅作装饰用途。--cov-fail-under - 覆盖率目标是多少,由谁设定? 无合理依据的目标(如“副总裁要求达到80%”)会导致投机行为。目标应反映风险容忍度和代码库成熟度,而非随意的整数。
Core Principles
核心原则
1. Coverage measures breadth, not depth. A line being executed does not mean it is tested correctly. executes the function but asserts nothing. Coverage tells you what code ran, not whether the tests would catch a bug — that is what mutation testing measures.
expect(true).toBe(true)2. Branch coverage matters more than line coverage. A ternary on one line counts as fully covered in line coverage even if only one branch ran. Line coverage does not guarantee branch coverage. Gate on branches, not just lines:
condition ? a : btypescript
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. Ratchet pattern: never decrease, only increase. Record current coverage as the minimum threshold. Every PR must meet or exceed it. Coverage climbs over time without forcing an artificial target up front.
4. Focus on gaps by risk, not on the number. A project at 85% is not automatically "better" than one at 75%. What matters is whether the untested slice contains payment, auth, or data-integrity logic. Analyze gaps by risk.
5. New code has a higher bar than legacy code. Require 90%+ on new code in PRs even if the project sits at 65%. This stops coverage decay without demanding a rewrite of legacy code.
1. 覆盖率衡量广度,而非深度。 代码行被执行并不意味着测试正确。执行了函数,但未做出任何有效断言。覆盖率仅能告诉你哪些代码被执行,无法判断测试能否捕获bug——这是突变测试的作用。
expect(true).toBe(true)2. 分支覆盖率比行覆盖率更重要。 一行三元表达式即使仅执行了一个分支,行覆盖率也会显示为100%。行覆盖率无法保证分支覆盖率。应基于分支覆盖率设置门禁,而非仅依赖行覆盖率:
condition ? a : btypescript
function discount(price: number, isPremium: boolean): number {
return isPremium ? price * 0.8 : price;
}
// 行覆盖率:100%(该行已执行)。分支覆盖率:50%(仅执行了true分支)。
expect(discount(100, true)).toBe(80);
// 修复——断言两条路径,使分支覆盖率达到100%:
expect(discount(100, true)).toBe(80);
expect(discount(100, false)).toBe(100);3. 棘轮模式:只升不降。 将当前覆盖率记录为最低阈值。每个PR必须达到或超过该阈值。覆盖率会随时间逐步提升,无需一开始就设定人工目标。
4. 聚焦风险导向的缺口,而非数字。 覆盖率85%的项目并不一定比75%的项目“更好”。关键在于未测试的部分是否包含支付、认证或数据完整性逻辑。需按风险分析覆盖率缺口。
5. 新代码的标准高于遗留代码。 即使项目整体覆盖率为65%,PR中的新代码仍需达到90%+的覆盖率。这样既能避免覆盖率下降,又无需重写遗留代码。
Coverage Tools
覆盖率工具
Pick the provider by test runner first, then by how cleanly it maps to your build output.
| Runner / context | Install | Provider |
|---|---|---|
| Vitest | | |
| Jest | bundled ( | V8 or Istanbul/babel |
Non-Vitest Node ( | | V8 via |
| Legacy Istanbul CLI | | Istanbul instrumentation |
| Python | | coverage.py |
Two engines underneath:
- V8 coverage — built into the V8 engine, so it does not instrument source: faster, no Babel transform. For Vitest, install (NOT
@vitest/coverage-v8— that is the standalone CLI for non-Vitest runners). For a plainc8or mocha project,node:testis the CLI wrapper around the same V8 data. Default for new Node/Vitest projects.c8 - Istanbul — instruments source code; slower but maps more reliably through transpilers and bundlers. Switch to it (, or
@vitest/coverage-istanbul) when V8 maps poorly. Symptom that V8 maps poorly: reported uncovered lines land on blank lines, closing braces, or decorators, or whole covered functions show as red — that means the source map is misattributing lines (common with certain TS bundlers / SWC configs). When you see that, flip to the Istanbul provider.nyc
Node baseline:11.x andc818.x are current. c8 11 still supports Node >=12; nyc 18 requires Node 20 || >= 22. If you must stay on Node 18, pinnyc(c8 11 runs fine on Node 18). New projects should standardize on Node 20+.nyc@^17
See for the full provider configs (Vitest block, , Jest , / ), install commands, and run invocations.
references/tool-config.mdcoverage.nycrc.jsoncoverageThresholdpyproject.toml.coveragerc.toml首先根据测试运行器选择工具,再根据其与构建输出的适配性决定。
| 运行器/环境 | 安装包 | 工具提供商 |
|---|---|---|
| Vitest | | |
| Jest | 内置( | V8或Istanbul/babel |
非Vitest Node( | | 通过 |
| 遗留Istanbul CLI | | Istanbul插桩 |
| Python | | coverage.py |
底层包含两种引擎:
- V8 coverage — 内置在V8引擎中,无需插桩源代码:速度更快,无需Babel转换。对于Vitest,安装(而非
@vitest/coverage-v8——c8是面向非Vitest运行器的独立CLI)。对于原生c8或mocha项目,node:test是基于相同V8数据的CLI封装。是新Node/Vitest项目的默认选择。c8 - Istanbul — 对源代码进行插桩;速度较慢,但在转译器和打包器中的映射更可靠。当V8映射效果不佳时,切换为Istanbul(或
@vitest/coverage-istanbul)。V8映射不佳的症状:报告的未覆盖行出现在空白行、闭合括号或装饰器上,或已覆盖的函数显示为红色——这意味着源映射错误归因了行号(在某些TS打包器/SWC配置中常见)。出现此情况时,切换为Istanbul提供商。nyc
Node版本基准:11.x和c818.x为当前版本。c8 11仍支持Node >=12;nyc 18要求Node 20 || >= 22。如果必须留在Node 18,请固定nyc(c8 11可在Node 18上正常运行)。新项目应标准化使用Node 20+。nyc@^17
完整的工具配置(Vitest 块、、Jest 、/)、安装命令和运行方式,请查看。
coverage.nycrc.jsoncoverageThresholdpyproject.toml.coveragerc.tomlreferences/tool-config.mdMerging coverage across test types
合并不同测试类型的覆盖率
Unit, integration, and E2E runs each produce partial coverage. Combine them so a line covered only by an integration test isn't reported as a gap:
- Vitest — run as multiple projects/configs and let Vitest merge, or merge outputs.
coverage-final.json - nyc — combines
nyc merge .nyc_output merged.json && nyc report -t mergedfiles from separate runs..json - coverage.py — after running each suite with
coverage combine.coverage run -p
Merge first, gate on the merged total. Don't gate each suite's coverage in isolation.
单元测试、集成测试和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
先合并覆盖率,再基于合并后的总覆盖率设置门禁。不要单独为每个测试套件设置覆盖率门禁。
Coverage Report Types
覆盖率报告类型
| Reporter | Output | Use Case |
|---|---|---|
| Terminal table | Quick local check |
| Interactive HTML | Detailed local analysis, clicking through files |
| | SonarQube, Codecov, Coveralls integration |
| | CI scripts, PR comments, dashboard metrics |
| | GitLab CI coverage visualization |
| 报告器 | 输出内容 | 使用场景 |
|---|---|---|
| 终端表格 | 快速本地检查 |
| 交互式HTML | 详细本地分析,可点击查看文件 |
| | 与SonarQube、Codecov、Coveralls集成 |
| | CI脚本、PR评论、仪表盘指标 |
| | GitLab CI覆盖率可视化 |
Gap Analysis
覆盖率缺口分析
Coverage reports show which lines and branches did not run. Not all gaps are equal — prioritize by risk.
Step 1: Generate the report.
bash
npm run test:coverage覆盖率报告会显示哪些行和分支未被执行。并非所有缺口都同等重要——需按风险优先级处理。
步骤1:生成报告。
bash
npm run test:coverageOpen coverage/index.html in a browser
在浏览器中打开coverage/index.html
**Step 2: Sort files by uncovered lines.** Parse `coverage-summary.json`, sort files by `(total - covered)` descending, focus on the top 20. A small script that reads the JSON summary and outputs file / line% / branch% / uncovered-count makes this repeatable.
**Step 3: Map gaps to risk.**
| Gap Location | Risk Level | Action |
|-------------|-----------|--------|
| Payment processing | Critical | Write tests immediately |
| Auth/permissions | Critical | Write tests immediately |
| Data validation | High | Add to next sprint |
| Error handling paths | High | Add to next sprint |
| Utility functions | Medium | Cover when modifying |
| UI formatting | Low | Skip unless regression-prone |
| Generated code | None | Exclude from coverage |
Include branch coverage in the sort, not just lines — a file at 100% line / 50% branch hides untested paths a line-only sort would rank as "done."
---
**步骤2:按未覆盖行数排序文件。** 解析`coverage-summary.json`,按`(总行数 - 已覆盖行数)`降序排序文件,重点关注前20个。可编写一个小型脚本读取JSON摘要,输出文件/行覆盖率/分支覆盖率/未覆盖行数,使该过程可重复。
**步骤3:将缺口映射到风险等级。**
| 缺口位置 | 风险等级 | 行动 |
|-------------|-----------|--------|
| 支付处理 | 关键 | 立即编写测试 |
| 认证/权限 | 关键 | 立即编写测试 |
| 数据验证 | 高 | 添加到下一个迭代 |
| 错误处理路径 | 高 | 添加到下一个迭代 |
| 工具函数 | 中 | 修改时补充覆盖 |
| UI格式化 | 低 | 除非易出现回归,否则跳过 |
| 生成代码 | 无 | 从覆盖率中排除 |
排序时需包含分支覆盖率,而非仅行覆盖率——一个行覆盖率100%但分支覆盖率50%的文件,其隐藏的未测试路径会被仅按行覆盖率排序的结果误判为“已完成”。
---Coverage as CI Gate
CI覆盖率门禁
Threshold Configuration
阈值配置
Set a global threshold as the project minimum, then layer per-directory thresholds stricter for critical code (payments, auth) than for utilities. Vitest uses glob keys under (e.g. ); Jest uses path keys under . Both support per-path overrides.
thresholds"src/payments/**": { lines: 95, branches: 90 }coverageThresholdSee for the global, per-directory, and Jest per-file threshold config.
references/ci-gating.md设置全局阈值作为项目最低要求,然后为关键代码(支付、认证)设置比工具代码更严格的目录级阈值。Vitest在下使用glob键(例如);Jest在下使用路径键。两者均支持路径级覆盖。
thresholds"src/payments/**": { lines: 95, branches: 90 }coverageThreshold全局、目录级和Jest文件级阈值配置,请查看。
references/ci-gating.mdRatchet Pattern
棘轮模式
Never let coverage decrease. Record the current level as the minimum and floor it upward when coverage improves. A ratchet script reads , compares each metric against a committed , fails the build on any regression, and updates the baseline when coverage improves.
coverage-summary.json.coverage-ratchet.jsonCommit (e.g. ). In CI, run the ratchet script after tests. On main-branch merges, auto-commit the updated ratchet file if coverage improved.
.coverage-ratchet.json{ "lines": 82, "branches": 78, ... }See for the full script.
references/ci-gating.mdcoverage-ratchet.ts绝不允许覆盖率下降。将当前覆盖率记录为最小值,当覆盖率提升时向上更新基准线。棘轮脚本读取,将每个指标与已提交的对比,若出现回归则构建失败,若覆盖率提升则更新基准线。
coverage-summary.json.coverage-ratchet.json提交(例如)。在CI中,测试完成后运行棘轮脚本。在主分支合并时,若覆盖率提升则自动提交更新后的棘轮文件。
.coverage-ratchet.json{ "lines": 82, "branches": 78, ... }完整的脚本,请查看。
coverage-ratchet.tsreferences/ci-gating.mdPR Diff Coverage Gate
PR差异覆盖率门禁
Require new code in a PR to meet a higher threshold (e.g. 90%) than the project baseline. In CI, use to identify changed files, then check their coverage from . Fail the pipeline if changed-file coverage falls below the threshold. This stops decay without rewriting legacy code.
git diff --name-only origin/main...HEADcoverage-summary.jsonSurface the diff to reviewers with (reads the JSON summary) or with a script that filters to changed files. See for the full PR workflow.
davelosert/vitest-coverage-report-action@v2marocchino/sticky-pull-request-comment@v2references/ci-gating.mdHosted alternatives: Codecov, Coveralls, and Trunk Coverage ship first-class differential PR coverage with merge-blocking gates and inline annotations. Most teams prefer these over hand-rolled diff scripts — pick one if you don't already have a coverage host. Codecov + GitHub: reads and posts a PR diff comment automatically.
codecov/codecov-action@v5lcov.info要求PR中的新代码达到比项目基准更高的阈值(例如90%)。在CI中,使用识别变更文件,然后从中检查它们的覆盖率。若变更文件的覆盖率低于阈值,则流水线失败。这样既能避免覆盖率下降,又无需重写遗留代码。
git diff --name-only origin/main...HEADcoverage-summary.json可使用(读取JSON摘要)或搭配筛选变更文件的脚本,向评审者展示差异覆盖率。完整的PR工作流,请查看。
davelosert/vitest-coverage-report-action@v2marocchino/sticky-pull-request-comment@v2references/ci-gating.md托管替代方案: Codecov、Coveralls和Trunk Coverage提供原生的差分PR覆盖率功能,包含合并阻断门禁和内联注释。大多数团队更倾向于使用这些工具而非自行编写差异脚本——如果尚未使用覆盖率托管工具,可选择其一。Codecov + GitHub:读取并自动在PR中发布差异评论。
codecov/codecov-action@v5lcov.infoMutation Testing
突变测试
Mutation testing measures assertion quality, not just code execution. It mutates your source (flips a to , deletes a line) and checks whether a test fails. A surviving mutant means a real bug your tests would miss. With Stryker JS v9.6+ and Vitest 4.1+ the cost is low enough to run on PR-changed files; mutmut 3.x covers Python.
>>=突变测试用于衡量断言质量,而非仅代码执行情况。它会修改源代码(例如将改为、删除一行),并检查测试是否失败。存活的突变体意味着存在测试无法发现的真实bug。使用**Stryker JS v9.6+和Vitest 4.1+**时,成本足够低,可在PR变更文件上运行;mutmut 3.x支持Python。
>>=Targeting
目标范围
Mutation testing is expensive on whole codebases — run it incrementally. Stryker's (JSON cache) plus scoped to the git diff re-mutates only touched files; mutmut similarly mutates per path. Restrict to:
incremental: true--mutate- Pure business logic (validators, calculators, transformers)
- Critical paths (payment, auth, data integrity)
- Code with high line coverage but suspect assertions (branch coverage > 90% but few assertion variants)
Skip UI rendering, glue code, and generated code.
See for the Stryker config (, incremental, run on changed files only) and the mutmut invocation.
references/mutation-testing.mdstryker.config.json对整个代码库进行突变测试成本高昂——需增量运行。Stryker的(JSON缓存)加上限定为git差异文件,仅重新突变被修改的文件;mutmut同样支持按路径突变。限制在以下范围:
incremental: true--mutate- 纯业务逻辑(验证器、计算器、转换器)
- 关键路径(支付、认证、数据完整性)
- 行覆盖率高但断言可疑的代码(分支覆盖率>90%但断言变体少)
跳过UI渲染、粘合代码和生成代码。
Stryker配置(、增量模式、仅在变更文件上运行)和mutmut调用方式,请查看。
stryker.config.jsonreferences/mutation-testing.mdReading the score
分数解读
A mutation score of 80% means 80% of injected bugs were caught. Lower than your coverage % is normal — many mutants land in untested branches the coverage report already flagged. The interesting signal is high coverage + low mutation score: code executes but assertions don't constrain it.
突变分数为80%意味着80%的注入bug被捕获。分数低于覆盖率百分比是正常的——许多突变体出现在覆盖率报告已标记的未测试分支中。值得关注的信号是高覆盖率+低突变分数:代码被执行,但断言未对其行为形成约束。
Meaningful vs Vanity Coverage
有意义vs表面覆盖率
Why 100% Coverage Is Usually Wrong
为什么100%覆盖率通常不可取
100% requires testing every branch of every line, including:
- Error handling for impossible states
- Default cases in exhaustive switches
- Framework lifecycle methods never called directly
- Defensive checks against corrupted data
Tests written to hit 100% are often trivial, brittle, and catch no real bugs.
100%覆盖率需要测试每一行的每个分支,包括:
- 不可能出现的状态的错误处理
- 穷举switch中的默认分支
- 从未直接调用的框架生命周期方法
- 针对损坏数据的防御性检查
为达到100%覆盖率编写的测试通常琐碎、脆弱,无法捕获真实bug。
Diminishing Returns
收益递减规律
| Coverage Range | Value | Effort |
|---|---|---|
| 0% to 60% | High — main paths, obvious regressions | Low |
| 60% to 80% | Medium — error paths, edge cases | Medium |
| 80% to 90% | Lower — unusual combinations, defensive code | High |
| 90% to 100% | Minimal — unreachable code, framework internals | Very high |
The sweet spot is 75–85% for most projects. Critical paths (payments, auth) aim higher (90%+). Set the global threshold in the sweet spot and per-directory thresholds at 90%+ for payment/auth.
| 覆盖率范围 | 价值 | 投入 |
|---|---|---|
| 0% 到 60% | 高——覆盖主路径、明显的回归问题 | 低 |
| 60% 到 80% | 中——覆盖错误路径、边缘情况 | 中 |
| 80% 到 90% | 较低——覆盖特殊组合、防御性代码 | 高 |
| 90% 到 100% | 极低——覆盖不可达代码、框架内部逻辑 | 极高 |
大多数项目的最佳区间是75–85%。关键路径(支付、认证)应设定更高目标(90%+)。将全局阈值设在最佳区间,为支付/认证目录设置90%+的阈值。
What NOT to Cover
无需覆盖的内容
Exclude these — they inflate the denominator without adding value. Document each exclusion's justification in a CONTRIBUTING/coverage note so the exclude list can't quietly hide real gaps.
typescript
// 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
]排除以下内容——它们会扩大分母但无实际价值。在CONTRIBUTING/coverage文档中记录每个排除项的理由,避免排除列表悄然隐藏真实缺口。
typescript
// vitest.config.ts / jest.config.js — 排除模式
exclude: [
"**/*.d.ts", // 类型定义
"**/index.ts", // 桶导出(仅重导出)
"**/*.stories.{ts,tsx}", // Storybook故事
"**/generated/**", // 自动生成代码(GraphQL、Prisma)
"**/migrations/**", // 数据库迁移
"**/__mocks__/**", // 测试模拟
"**/types/**", // 仅类型模块
]Quality Indicators Beyond Percentage
百分比之外的质量指标
| Indicator | What It Measures | How to Get It |
|---|---|---|
| Mutation score | Would tests catch a real bug? | Stryker / mutmut |
| Branch coverage | Are all conditional paths tested? | V8/Istanbul with branch reporting |
| Critical path coverage | Are payment/auth/data flows fully covered? | Per-directory thresholds |
| Defect escape rate | Do production bugs occur in tested code? | Post-incident analysis |
| Coverage delta | Is coverage improving or declining? | Ratchet pattern tracking |
| 指标 | 衡量内容 | 获取方式 |
|---|---|---|
| 突变分数 | 测试能否捕获真实bug? | Stryker / mutmut |
| 分支覆盖率 | 所有条件路径是否都被测试? | 启用分支报告的V8/Istanbul |
| 关键路径覆盖率 | 支付/认证/数据流是否被完全覆盖? | 目录级阈值 |
| 缺陷逃逸率 | 生产环境bug是否出现在已测试代码中? | 事后事件分析 |
| 覆盖率变化量 | 覆盖率是提升还是下降? | 棘轮模式跟踪 |
Anti-Patterns
反模式
1. Treating coverage as proof of quality
1. 将覆盖率视为质量的证明
"We have 90% coverage so we're well-tested" is dangerous. Coverage says code executed, not that behavior was verified. Fix: pair the percentage with a mutation score on critical modules; a test with no assertions should drop the mutation score even at 100% line coverage.
“我们有90%的覆盖率,所以测试很完善”是危险的。覆盖率仅表明代码被执行,不意味着行为被验证。修复方案: 将覆盖率百分比与关键模块的突变分数结合;即使行覆盖率为100%,无断言的测试也应降低突变分数。
2. Excluding files to inflate numbers
2. 通过排除文件来抬高覆盖率数字
Adding hard-to-test files (error handlers, integration modules) to the exclude list hides the most important gaps. Fix: only exclude genuinely untestable code — generated files, type definitions, barrel exports — and justify each exclusion in writing.
将难以测试的文件(错误处理程序、集成模块)加入排除列表,会隐藏最重要的缺口。修复方案: 仅排除真正无法测试的代码——生成文件、类型定义、桶导出——并书面记录每个排除项的理由。
3. Writing trivial tests to hit targets
3. 编写琐碎测试以达到目标
it("should exist", () => expect(MyClass).toBeDefined())it("should exist", () => expect(MyClass).toBeDefined())4. Global threshold without per-module analysis
4. 仅设置全局阈值而不进行模块级分析
An 80% global threshold passes even if payments sits at 30%, as long as utilities inflate the average. Fix: per-directory thresholds at 90%+ for payment/auth.
80%的全局阈值可能在支付模块覆盖率仅30%的情况下通过,只要工具代码拉高了平均值。修复方案: 为支付/认证目录设置90%+的目录级阈值。
5. Coverage threshold set once, never adjusted
5. 覆盖率阈值设置后从未调整
A team stuck at 78% for six months isn't improving. Fix: the ratchet floors upward automatically; review it quarterly and investigate stagnation.
团队连续六个月停留在78%的覆盖率,说明没有进步。修复方案: 棘轮机制会自动向上更新基准线;每季度回顾基准线,调查停滞原因。
6. Ignoring branch coverage
6. 忽略分支覆盖率
Line coverage reports 100% on even if one branch never runs. Fix: always report and gate on alongside .
const r = cond ? a : bbrancheslines对于,即使仅执行一个分支,行覆盖率仍会显示100%。修复方案: 始终报告并基于和设置门禁。
const r = cond ? a : bbrancheslines7. Coverage from E2E tests only
7. 仅依赖E2E测试的覆盖率
A single E2E test touches 60% of the codebase without testing any edge case — broad and shallow. Fix: measure unit/integration coverage separately from E2E and gate on the unit/integration total; E2E coverage is a bonus signal, not the gate.
单个E2E测试可能触及60%的代码库,但未测试任何边缘情况——覆盖面广但深度不足。修复方案: 单独衡量单元/集成覆盖率与E2E覆盖率,并基于单元/集成总覆盖率设置门禁;E2E覆盖率作为额外信号,而非门禁依据。
Verification
验证
Prove the gate actually fails on regression — a green build with no enforcement is worthless.
- Threshold fires: temporarily lower one committed metric below current (or delete one passing test), run (or
npm run test:coverage), and confirm a non-zero exit code. Restore afterward.pytest --cov=src --cov-fail-under=80 - Ratchet fires: with committed, drop a test so coverage regresses, run the ratchet script, and confirm it prints
.coverage-ratchet.jsonand exits 1.FAIL: ... coverage dropped - Branch gate is on: confirm the report shows a column and that
branchesappears in the threshold config — not justbranches.lines - PR diff renders: open a draft PR touching one source file and confirm the coverage-diff comment posts with the changed file's numbers.
If step 1 exits 0 after you lowered coverage, the gate is not wired — fix that before claiming Done.
需证明门禁确实会在回归时触发——无强制力的绿色构建毫无价值。
- 阈值触发: 临时将某个已提交的指标调低至当前水平以下(或删除一个通过的测试),运行(或
npm run test:coverage),确认返回非零退出码。之后恢复设置。pytest --cov=src --cov-fail-under=80 - 棘轮触发: 在已提交的情况下,删除一个测试使覆盖率倒退,运行棘轮脚本,确认输出
.coverage-ratchet.json并以状态码1退出。FAIL: ... coverage dropped - 分支门禁启用: 确认报告显示列,且阈值配置中包含
branches——而非仅branches。lines - PR差异显示: 打开一个修改单个源文件的草稿PR,确认覆盖率差异评论已发布,并显示变更文件的覆盖率数据。
如果步骤1在降低覆盖率后返回0,说明门禁未正确配置——在标记完成前修复该问题。
Done When
完成标准
- Coverage runs automatically in CI on every push (no manual step to generate the report).
- Coverage threshold is enforced in CI: the build exits non-zero when line or branch coverage drops below the defined minimum (verified per Verification step 1).
- Coverage report is published as a CI artifact (HTML + ) and a per-PR coverage delta posts to PR comments.
json-summary - The coverage config contains an exclude list and per-directory thresholds (90%+ for payments/auth), and a CONTRIBUTING/coverage doc lists each exclusion's justification.
- Ratchet is wired: is committed, the ratchet script runs in CI, and the build fails on regression from the recorded baseline (verified per Verification step 2).
.coverage-ratchet.json
- 覆盖率在每次推送时自动在CI中运行(无需手动生成报告)。
- CI中强制实施覆盖率阈值:当行或分支覆盖率低于定义的最小值时,构建以非零状态退出(已通过验证步骤1确认)。
- 覆盖率报告作为CI工件发布(HTML + ),且PR级覆盖率变化量会发布到PR评论中。
json-summary - 覆盖率配置包含排除列表和目录级阈值(支付/认证为90%+),且CONTRIBUTING/coverage文档列出了每个排除项的理由。
- 棘轮机制已配置:已提交,棘轮脚本在CI中运行,且当覆盖率低于记录的基准线时构建失败(已通过验证步骤2确认)。
.coverage-ratchet.json
Reference Files (in references/
)
references/参考文件(位于references/
)
references/- tool-config.md — Full provider configs for Vitest (/
@vitest/coverage-v8), the c8 CLI for non-Vitest runners, nyc, Jest, and coverage.py, with install and run commands.-istanbul - ci-gating.md — PR coverage-diff workflow, global/per-directory/per-file thresholds, and the script.
coverage-ratchet.ts - mutation-testing.md — Stryker (, incremental) and mutmut configuration for measuring assertion quality.
stryker.config.json
- tool-config.md — Vitest(/
@vitest/coverage-v8)、非Vitest运行器的c8 CLI、nyc、Jest和coverage.py的完整工具配置,包含安装和运行命令。-istanbul - ci-gating.md — PR覆盖率差异工作流、全局/目录级/文件级阈值,以及脚本。
coverage-ratchet.ts - mutation-testing.md — Stryker(、增量模式)和mutmut的配置,用于衡量断言质量。
stryker.config.json
Related Skills
相关技能
- unit-testing — Writing the tests that raise coverage: mocking strategies, framework-specific config, and Vitest for changed-files-only coverage in CI. Go there to author tests; this skill measures and gates them.
coverage.changed - ci-cd-integration — Pipeline wiring for coverage gates, artifact storage, and PR comments.
- qa-metrics — Coverage as a tracked KPI trend over time alongside mutation score and defect escape rate. Go there for dashboards and trends; this skill is per-repo configuration and gating.
- ai-qa-review — AI-assisted identification of undertested paths and Vitest browser mode for component coverage parity.
- unit-testing — 编写提升覆盖率的测试:模拟策略、框架特定配置,以及Vitest 用于CI中仅检查变更文件的覆盖率。编写测试请使用该技能;本技能专注于衡量和门禁覆盖率。
coverage.changed - ci-cd-integration — 覆盖率门禁的流水线配置、工件存储和PR评论。
- qa-metrics — 将覆盖率作为长期跟踪的KPI趋势,与突变分数和缺陷逃逸率一起监控。如需仪表盘和趋势分析请使用该技能;本技能专注于单仓库的配置和门禁。
- ai-qa-review — AI辅助识别测试不足的路径,以及Vitest浏览器模式用于组件覆盖率一致性。