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

快速指引

SituationGo to
Pick and configure a coverage providerCoverage Tools →
references/tool-config.md
Decide where to write tests nextGap Analysis
Stop coverage from regressing in CICoverage as CI Gate → Ratchet Pattern
Show per-PR coverage to reviewersCoverage as CI Gate → PR Diff
Tests run code but don't assertMutation Testing
Decide what to exclude / what target to setMeaningful vs Vanity Coverage

场景参考内容
选择并配置覆盖率工具Coverage Tools →
references/tool-config.md
确定下一步测试编写方向覆盖率缺口分析
防止CI中覆盖率倒退CI覆盖率门禁 → 棘轮模式
向评审者展示PR级覆盖率CI覆盖率门禁 → PR差异检查
测试执行代码但未添加断言突变测试
确定排除范围/设置覆盖率目标有意义vs表面覆盖率

Discovery Questions

调研问题

Check
.agents/qa-project-context.md
first — if it exists, use it and skip anything already answered there. Then:
  1. What test runner and coverage tooling is configured? Check for
    vitest.config.*
    (coverage block),
    jest.config.*
    (coverageProvider),
    .nycrc
    ,
    c8
    in scripts, or
    [tool.coverage]
    in
    pyproject.toml
    . The runner decides the install — Vitest pulls
    @vitest/coverage-v8
    , not c8 (see Coverage Tools).
  2. 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.
  3. Is coverage gated in CI? Check GitHub Actions / GitLab CI for
    --coverage
    ,
    coverageThreshold
    ,
    fail_under
    , or
    --cov-fail-under
    . No gate means coverage is decorative.
  4. 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
——如果存在,使用其中内容并跳过已回答的问题。然后:
  1. 当前配置了什么测试运行器和覆盖率工具? 检查是否存在
    vitest.config.*
    (coverage配置块)、
    jest.config.*
    (coverageProvider)、
    .nycrc
    、脚本中的
    c8
    ,或
    pyproject.toml
    中的
    [tool.coverage]
    。测试运行器决定了安装方式——Vitest需安装
    @vitest/coverage-v8
    ,而非c8(详见覆盖率工具章节)。
  2. 当前覆盖率水平是多少? 运行现有覆盖率命令,记录行、分支和函数覆盖率百分比。这将作为棘轮机制的基准线。
  3. CI中是否设置了覆盖率门禁? 检查GitHub Actions/GitLab CI中是否存在
    --coverage
    coverageThreshold
    fail_under
    --cov-fail-under
    。无门禁意味着覆盖率仅作装饰用途。
  4. 覆盖率目标是多少,由谁设定? 无合理依据的目标(如“副总裁要求达到80%”)会导致投机行为。目标应反映风险容忍度和代码库成熟度,而非随意的整数。

Core Principles

核心原则

1. Coverage measures breadth, not depth. A line being executed does not mean it is tested correctly.
expect(true).toBe(true)
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.
2. Branch coverage matters more than line coverage. A ternary
condition ? a : b
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:
typescript
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. 覆盖率衡量广度,而非深度。 代码行被执行并不意味着测试正确。
expect(true).toBe(true)
执行了函数,但未做出任何有效断言。覆盖率仅能告诉你哪些代码被执行,无法判断测试能否捕获bug——这是突变测试的作用。
2. 分支覆盖率比行覆盖率更重要。 一行三元表达式
condition ? a : b
即使仅执行了一个分支,行覆盖率也会显示为100%。行覆盖率无法保证分支覆盖率。应基于分支覆盖率设置门禁,而非仅依赖行覆盖率:
typescript
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 / contextInstallProvider
Vitest
@vitest/coverage-v8
(default) or
@vitest/coverage-istanbul
coverage.provider: 'v8'
/
'istanbul'
Jestbundled (
coverageProvider: 'v8'
or
'babel'
)
V8 or Istanbul/babel
Non-Vitest Node (
node:test
, plain mocha)
c8
CLI
V8 via
c8 <command>
Legacy Istanbul CLI
nyc
Istanbul instrumentation
Python
pytest-cov
(wraps coverage.py)
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
    @vitest/coverage-v8
    (NOT
    c8
    — that is the standalone CLI for non-Vitest runners). For a plain
    node:test
    or mocha project,
    c8
    is the CLI wrapper around the same V8 data. Default for new Node/Vitest projects.
  • Istanbul — instruments source code; slower but maps more reliably through transpilers and bundlers. Switch to it (
    @vitest/coverage-istanbul
    , or
    nyc
    ) 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.
Node baseline:
c8
11.x and
nyc
18.x are current. c8 11 still supports Node >=12; nyc 18 requires Node 20 || >= 22. If you must stay on Node 18, pin
nyc@^17
(c8 11 runs fine on Node 18). New projects should standardize on Node 20+.
See
references/tool-config.md
for the full provider configs (Vitest
coverage
block,
.nycrc.json
, Jest
coverageThreshold
,
pyproject.toml
/
.coveragerc.toml
), install commands, and run invocations.
首先根据测试运行器选择工具,再根据其与构建输出的适配性决定。
运行器/环境安装包工具提供商
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 coverage — 内置在V8引擎中,无需插桩源代码:速度更快,无需Babel转换。对于Vitest,安装
    @vitest/coverage-v8
    (而非
    c8
    ——
    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

Merging 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
    coverage-final.json
    outputs.
  • nyc
    nyc merge .nyc_output merged.json && nyc report -t merged
    combines
    .json
    files from separate runs.
  • coverage.py
    coverage combine
    after running each suite with
    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

覆盖率报告类型

ReporterOutputUse Case
text
Terminal tableQuick local check
html
Interactive HTMLDetailed local analysis, clicking through files
lcov
lcov.info
file
SonarQube, Codecov, Coveralls integration
json-summary
coverage-summary.json
CI scripts, PR comments, dashboard metrics
cobertura
cobertura-coverage.xml
GitLab CI coverage visualization

报告器输出内容使用场景
text
终端表格快速本地检查
html
交互式HTML详细本地分析,可点击查看文件
lcov
lcov.info
文件
与SonarQube、Codecov、Coveralls集成
json-summary
coverage-summary.json
CI脚本、PR评论、仪表盘指标
cobertura
cobertura-coverage.xml
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:coverage

Open 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
thresholds
(e.g.
"src/payments/**": { lines: 95, branches: 90 }
); Jest uses path keys under
coverageThreshold
. Both support per-path overrides.
See
references/ci-gating.md
for the global, per-directory, and Jest per-file threshold config.
设置全局阈值作为项目最低要求,然后为关键代码(支付、认证)设置比工具代码更严格的目录级阈值。Vitest在
thresholds
下使用glob键(例如
"src/payments/**": { lines: 95, branches: 90 }
);Jest在
coverageThreshold
下使用路径键。两者均支持路径级覆盖。
全局、目录级和Jest文件级阈值配置,请查看
references/ci-gating.md

Ratchet Pattern

棘轮模式

Never let coverage decrease. Record the current level as the minimum and floor it upward when coverage improves. A ratchet script reads
coverage-summary.json
, compares each metric against a committed
.coverage-ratchet.json
, fails the build on any regression, and updates the baseline when coverage improves.
Commit
.coverage-ratchet.json
(e.g.
{ "lines": 82, "branches": 78, ... }
). In CI, run the ratchet script after tests. On main-branch merges, auto-commit the updated ratchet file if coverage improved.
See
references/ci-gating.md
for the full
coverage-ratchet.ts
script.
绝不允许覆盖率下降。将当前覆盖率记录为最小值,当覆盖率提升时向上更新基准线。棘轮脚本读取
coverage-summary.json
,将每个指标与已提交的
.coverage-ratchet.json
对比,若出现回归则构建失败,若覆盖率提升则更新基准线。
提交
.coverage-ratchet.json
(例如
{ "lines": 82, "branches": 78, ... }
)。在CI中,测试完成后运行棘轮脚本。在主分支合并时,若覆盖率提升则自动提交更新后的棘轮文件。
完整的
coverage-ratchet.ts
脚本,请查看
references/ci-gating.md

PR 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
git diff --name-only origin/main...HEAD
to identify changed files, then check their coverage from
coverage-summary.json
. Fail the pipeline if changed-file coverage falls below the threshold. This stops decay without rewriting legacy code.
Surface the diff to reviewers with
davelosert/vitest-coverage-report-action@v2
(reads the JSON summary) or
marocchino/sticky-pull-request-comment@v2
with a script that filters to changed files. See
references/ci-gating.md
for the full PR workflow.
Hosted 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:
codecov/codecov-action@v5
reads
lcov.info
and posts a PR diff comment automatically.

要求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
托管替代方案: CodecovCoverallsTrunk Coverage提供原生的差分PR覆盖率功能,包含合并阻断门禁和内联注释。大多数团队更倾向于使用这些工具而非自行编写差异脚本——如果尚未使用覆盖率托管工具,可选择其一。Codecov + GitHub:
codecov/codecov-action@v5
读取
lcov.info
并自动在PR中发布差异评论。

Mutation 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
incremental: true
(JSON cache) plus
--mutate
scoped to the git diff re-mutates only touched files; mutmut similarly mutates per path. Restrict to:
  • 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
references/mutation-testing.md
for the Stryker config (
stryker.config.json
, incremental, run on changed files only) and the mutmut invocation.
对整个代码库进行突变测试成本高昂——需增量运行。Stryker的
incremental: true
(JSON缓存)加上
--mutate
限定为git差异文件,仅重新突变被修改的文件;mutmut同样支持按路径突变。限制在以下范围:
  • 纯业务逻辑(验证器、计算器、转换器)
  • 关键路径(支付、认证、数据完整性)
  • 行覆盖率高但断言可疑的代码(分支覆盖率>90%但断言变体少)
跳过UI渲染、粘合代码和生成代码。
Stryker配置(
stryker.config.json
、增量模式、仅在变更文件上运行)和mutmut调用方式,请查看
references/mutation-testing.md

Reading 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 RangeValueEffort
0% to 60%High — main paths, obvious regressionsLow
60% to 80%Medium — error paths, edge casesMedium
80% to 90%Lower — unusual combinations, defensive codeHigh
90% to 100%Minimal — unreachable code, framework internalsVery 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

百分比之外的质量指标

IndicatorWhat It MeasuresHow to Get It
Mutation scoreWould tests catch a real bug?Stryker / mutmut
Branch coverageAre all conditional paths tested?V8/Istanbul with branch reporting
Critical path coverageAre payment/auth/data flows fully covered?Per-directory thresholds
Defect escape rateDo production bugs occur in tested code?Post-incident analysis
Coverage deltaIs 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())
adds coverage without value. Fix: every test verifies a behavior that, if broken, affects users; mutation testing flags these no-op tests.
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
const r = cond ? a : b
even if one branch never runs. Fix: always report and gate on
branches
alongside
lines
.
对于
const r = cond ? a : b
,即使仅执行一个分支,行覆盖率仍会显示100%。修复方案: 始终报告并基于
branches
lines
设置门禁。

7. 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.
  1. Threshold fires: temporarily lower one committed metric below current (or delete one passing test), run
    npm run test:coverage
    (or
    pytest --cov=src --cov-fail-under=80
    ), and confirm a non-zero exit code. Restore afterward.
  2. Ratchet fires: with
    .coverage-ratchet.json
    committed, drop a test so coverage regresses, run the ratchet script, and confirm it prints
    FAIL: ... coverage dropped
    and exits 1.
  3. Branch gate is on: confirm the report shows a
    branches
    column and that
    branches
    appears in the threshold config — not just
    lines
    .
  4. 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.

需证明门禁确实会在回归时触发——无强制力的绿色构建毫无价值。
  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,说明门禁未正确配置——在标记完成前修复该问题。

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 +
    json-summary
    ) and a per-PR coverage delta posts to PR comments.
  • 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:
    .coverage-ratchet.json
    is committed, the ratchet script runs in CI, and the build fails on regression from the recorded baseline (verified per Verification step 2).
  • 覆盖率在每次推送时自动在CI中运行(无需手动生成报告)。
  • CI中强制实施覆盖率阈值:当行或分支覆盖率低于定义的最小值时,构建以非零状态退出(已通过验证步骤1确认)。
  • 覆盖率报告作为CI工件发布(HTML +
    json-summary
    ),且PR级覆盖率变化量会发布到PR评论中。
  • 覆盖率配置包含排除列表和目录级阈值(支付/认证为90%+),且CONTRIBUTING/coverage文档列出了每个排除项的理由。
  • 棘轮机制已配置:
    .coverage-ratchet.json
    已提交,棘轮脚本在CI中运行,且当覆盖率低于记录的基准线时构建失败(已通过验证步骤2确认)。

Reference Files (in
references/
)

参考文件(位于
references/

  • tool-config.md — Full provider configs for Vitest (
    @vitest/coverage-v8
    /
    -istanbul
    ), the c8 CLI for non-Vitest runners, nyc, Jest, and coverage.py, with install and run commands.
  • ci-gating.md — PR coverage-diff workflow, global/per-directory/per-file thresholds, and the
    coverage-ratchet.ts
    script.
  • mutation-testing.md — Stryker (
    stryker.config.json
    , incremental) and mutmut configuration for measuring assertion quality.
  • 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的配置,用于衡量断言质量。

Related Skills

相关技能

  • unit-testing — Writing the tests that raise coverage: mocking strategies, framework-specific config, and Vitest
    coverage.changed
    for changed-files-only coverage in CI. Go there to author tests; this skill measures and gates them.
  • 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
    coverage.changed
    用于CI中仅检查变更文件的覆盖率。编写测试请使用该技能;本技能专注于衡量和门禁覆盖率。
  • ci-cd-integration — 覆盖率门禁的流水线配置、工件存储和PR评论。
  • qa-metrics — 将覆盖率作为长期跟踪的KPI趋势,与突变分数和缺陷逃逸率一起监控。如需仪表盘和趋势分析请使用该技能;本技能专注于单仓库的配置和门禁。
  • ai-qa-review — AI辅助识别测试不足的路径,以及Vitest浏览器模式用于组件覆盖率一致性。