test-coverage

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Test Coverage

测试覆盖率

Audit gaps, write targeted tests, enforce thresholds — across any ecosystem.
审计缺口、编写针对性测试、强制执行阈值 —— 跨任意生态体系适用。

Mental Model

思维模型

The testing pyramid encodes an economic truth: each tier tests what only it can test.
TierTestsCost to writeCost to run
UnitPure functions, domain logic, validation, parsingLowMilliseconds
IntegrationDatabase queries, API boundaries, access control, service interactionsMediumSeconds
ComponentRendered UI in a real browser, user interactions, visual statesMediumSeconds
E2EFull user flows across the entire stackHighMinutes
Coverage is a regression gate, not a quality metric. High coverage with bad tests is worse than moderate coverage with good tests. The goal is: new code cannot silently skip tests.
Exclusions are architecture, not exceptions. Every exclusion documents a deliberate decision about where code is tested. An exclusion at one tier should have coverage at another.
测试金字塔蕴含着一个经济性真理:每个层级只测试其独有的可测试内容。
层级测试对象编写成本运行成本
单元测试纯函数、领域逻辑、校验、解析毫秒级
集成测试数据库查询、API边界、访问控制、服务交互秒级
组件测试真实浏览器中渲染的UI、用户交互、视觉状态秒级
E2E测试跨整个技术栈的完整用户流程分钟级
覆盖率是回归防护门,而非质量指标。 高覆盖率搭配糟糕的测试,比中等覆盖率搭配优质测试更差。我们的目标是:新代码不能悄无声息地跳过测试。
排除项是架构设计,而非例外情况。 每个排除项都记录了关于代码测试位置的深思熟虑的决策,某一层级的排除项应当在另一层级有对应的覆盖率。

Decision Tree

决策树

Start here. Follow the branch that matches the current state.
text
Is there any coverage tooling configured?
├── No → Bootstrap (below)
└── Yes
    ├── Coverage below target? → Audit & Improve (below)
    ├── Coverage adequate but not enforced? → Enforce (below)
    └── Coverage enforced, writing new code? → Write Tests for New Code (below)
从这里开始,沿着符合当前状态的分支执行。
text
Is there any coverage tooling configured?
├── No → Bootstrap (below)
└── Yes
    ├── Coverage below target? → Audit & Improve (below)
    ├── Coverage adequate but not enforced? → Enforce (below)
    └── Coverage enforced, writing new code? → Write Tests for New Code (below)

Bootstrap: Setting Up Coverage from Scratch

从零搭建:从头开始配置测试覆盖率

1. Detect the ecosystem

1. 识别技术生态

Check for project markers:
package.json
,
go.mod
,
Cargo.toml
,
pyproject.toml
,
setup.py
,
*.csproj
. See ecosystem patterns for tool recommendations per language.
检查项目标识文件:
package.json
go.mod
Cargo.toml
pyproject.toml
setup.py
*.csproj
。查阅生态体系模式获取各语言的工具推荐。

2. Create tiered configs

2. 创建分层配置

Each test tier gets its own configuration file with targeted include/exclude patterns. This prevents slow integration tests from blocking fast unit test feedback.
Key principles:
  • Each tier has a separate
    include
    pattern matching only its source files
  • Each tier has a separate coverage output directory (avoids conflicts)
  • CI vs local reporter selection: text-summary locally, full HTML/JSON/LCOV in CI
TypeScript/Vitest example structure:
text
vitest.unit.config.mts    → tests/unit/**/*.unit.spec.ts    → coverage/unit/
vitest.int.config.mts     → tests/int/**/*.int.spec.ts      → coverage/int/
vitest.browser.config.mts → tests/components/**/*.spec.tsx   → coverage/components/
Python example:
bash
pytest -m unit --cov --cov-report=html:coverage/unit
pytest -m integration --cov --cov-report=html:coverage/int
每个测试层级都有专属的配置文件,配置针对性的包含/排除规则。这可以避免运行缓慢的集成测试阻塞快速的单元测试反馈。
核心原则:
  • 每个层级都有独立的
    include
    规则,仅匹配其对应的源文件
  • 每个层级都有独立的覆盖率输出目录(避免冲突)
  • CI与本地报告器选择:本地使用text-summary,CI中使用完整的HTML/JSON/LCOV报告
TypeScript/Vitest 示例结构:
text
vitest.unit.config.mts    → tests/unit/**/*.unit.spec.ts    → coverage/unit/
vitest.int.config.mts     → tests/int/**/*.int.spec.ts      → coverage/int/
vitest.browser.config.mts → tests/components/**/*.spec.tsx   → coverage/components/
Python示例:
bash
pytest -m unit --cov --cov-report=html:coverage/unit
pytest -m integration --cov --cov-report=html:coverage/int

3. Set initial thresholds

3. 设置初始阈值

Run coverage once, note the baseline. Set thresholds at the current level — this prevents regression while you improve.
text
undefined
运行一次覆盖率统计,记录基线值。将阈值设置为当前基线水平——这可以在你改进覆盖率的过程中防止回归。
text
undefined

Example: start where you are

Example: start where you are

thresholds: { lines: 72 } # measured baseline

Then ratchet up as you add tests. Never ratchet down. See [enforcement](references/enforcement.md) for the full ratcheting strategy.
thresholds: { lines: 72 } # measured baseline

然后逐步在新增测试时提升阈值,永远不要降低阈值。查阅[强制执行](references/enforcement.md)获取完整的阈值逐步提升策略。

4. Add coverage scripts

4. 新增覆盖率脚本

Create per-tier scripts in your project manifest:
json
{
  "test:unit": "vitest run --config ./vitest.unit.config.mts",
  "test:unit:coverage": "vitest run --coverage --config ./vitest.unit.config.mts",
  "test:int": "vitest run --config ./vitest.int.config.mts",
  "test:int:coverage": "vitest run --coverage --config ./vitest.int.config.mts",
  "test:components": "vitest run --coverage --config ./vitest.browser.config.mts",
  "test:e2e": "playwright test",
  "test": "pnpm test:unit && pnpm test:int && pnpm test:components && pnpm test:e2e"
}
在项目 manifest 文件中创建分层测试脚本:
json
{
  "test:unit": "vitest run --config ./vitest.unit.config.mts",
  "test:unit:coverage": "vitest run --coverage --config ./vitest.unit.config.mts",
  "test:int": "vitest run --config ./vitest.int.config.mts",
  "test:int:coverage": "vitest run --coverage --config ./vitest.int.config.mts",
  "test:components": "vitest run --coverage --config ./vitest.browser.config.mts",
  "test:e2e": "playwright test",
  "test": "pnpm test:unit && pnpm test:int && pnpm test:components && pnpm test:e2e"
}

Audit & Improve: Closing Coverage Gaps

审计与改进:填补覆盖率缺口

Phase 1: Audit

阶段1:审计

Run coverage for each tier and examine the output.
bash
undefined
为每个层级运行覆盖率统计,检查输出结果。
bash
undefined

Run with coverage, examine the HTML report or text output

Run with coverage, examine the HTML report or text output

<runner> --coverage

Identify three categories:

- **Untested files** — no coverage at all (highest priority)
- **Untested branches** — code paths never exercised
- **Untested functions** — declared but never called in tests
<runner> --coverage

识别三类问题:

- **未测试文件** —— 完全没有覆盖率(最高优先级)
- **未测试分支** —— 从未被执行过的代码路径
- **未测试函数** —— 已声明但从未在测试中调用的函数

Phase 2: Classify each gap

阶段2:分类每个缺口

For every uncovered file or function, ask:
QuestionIf yesIf no
Business logic or domain rules?Unit tests (highest priority)Continue
Access control or authorisation?Integration testsContinue
Data validation or parsing?Unit testsContinue
API endpoint or mutation?Integration testsContinue
UI component with logic?Component testsContinue
Full user flow?E2E testsContinue
Can it run in the test environment?Write testsDocument exclusion
Auto-generated code?Exclude with commentWrite tests
Thin wrapper around tested library?Consider excludingWrite tests
对每个未覆盖的文件或函数,询问以下问题:
问题
业务逻辑或领域规则?单元测试(最高优先级)继续判断
访问控制或鉴权?集成测试继续判断
数据校验或解析?单元测试继续判断
API端点或变更操作?集成测试继续判断
带逻辑的UI组件?组件测试继续判断
完整用户流程?E2E测试继续判断
能否在测试环境中运行?编写测试记录为排除项
自动生成的代码?加注释排除编写测试
已测试库的薄封装?考虑排除编写测试

Phase 3: Prioritise

阶段3:优先级排序

Triage order (highest value first):
  1. Domain logic and business rules (unit)
  2. Access control and authorisation (integration)
  3. Data validation and input parsing (unit)
  4. API endpoints and mutations (integration)
  5. UI components with conditional logic (component)
  6. Async/server-rendered components (E2E)
  7. Configuration and wiring (tested implicitly by higher tiers)
处理顺序(价值从高到低):
  1. 领域逻辑和业务规则(单元测试)
  2. 访问控制和鉴权(集成测试)
  3. 数据校验和输入解析(单元测试)
  4. API端点和变更操作(集成测试)
  5. 带条件逻辑的UI组件(组件测试)
  6. 异步/服务端渲染组件(E2E测试)
  7. 配置和装配代码(由更高层级的测试隐式覆盖)

Phase 4: Write tests

阶段4:编写测试

For each gap, follow the appropriate tier's patterns. Test expected behaviour through the public API, not implementation details.
Unit tests: Pure input → output. No database, no network, no filesystem.
typescript
describe('slugify', () => {
  it('converts spaces to hyphens', () => {
    expect(slugify('hello world')).toBe('hello-world')
  })
  it('handles empty string', () => {
    expect(slugify('')).toBe('')
  })
})
Integration tests: Real database, real service boundaries, no mocks for things you own.
typescript
it('enforces access control on draft posts', async () => {
  const result = await payload.find({
    collection: 'posts',
    where: { _status: { equals: 'draft' } },
    overrideAccess: false,
    user: anonymousUser,
  })
  expect(result.docs).toHaveLength(0)
})
Component tests: Real browser, real DOM queries (accessibility-first via testing-library).
tsx
it('renders film title and year', () => {
  render(<FilmCard film={mockFilm} />)
  expect(screen.getByText('Film Title')).toBeInTheDocument()
  expect(screen.getByText('2024')).toBeInTheDocument()
})
E2E tests: Full user flows, real navigation, real network.
typescript
test('user can submit a form', async ({ page }) => {
  await page.goto('/submit')
  await page.fill('[name="title"]', 'My Film')
  await page.click('button[type="submit"]')
  await expect(page).toHaveURL(/\/confirmation/)
})
See ecosystem patterns for language-specific runner syntax and config examples.
对每个缺口,遵循对应层级的模式。通过公共API测试预期行为,而非测试实现细节。
单元测试: 纯输入→输出,无数据库、无网络、无文件系统操作。
typescript
describe('slugify', () => {
  it('converts spaces to hyphens', () => {
    expect(slugify('hello world')).toBe('hello-world')
  })
  it('handles empty string', () => {
    expect(slugify('')).toBe('')
  })
})
集成测试: 真实数据库、真实服务边界,不对自有代码做mock。
typescript
it('enforces access control on draft posts', async () => {
  const result = await payload.find({
    collection: 'posts',
    where: { _status: { equals: 'draft' } },
    overrideAccess: false,
    user: anonymousUser,
  })
  expect(result.docs).toHaveLength(0)
})
组件测试: 真实浏览器、真实DOM查询(通过testing-library优先保证可访问性)。
tsx
it('renders film title and year', () => {
  render(<FilmCard film={mockFilm} />)
  expect(screen.getByText('Film Title')).toBeInTheDocument()
  expect(screen.getByText('2024')).toBeInTheDocument()
})
E2E测试: 完整用户流程、真实导航、真实网络请求。
typescript
test('user can submit a form', async ({ page }) => {
  await page.goto('/submit')
  await page.fill('[name="title"]', 'My Film')
  await page.click('button[type="submit"]')
  await expect(page).toHaveURL(/\/confirmation/)
})
查阅生态体系模式获取特定语言的运行器语法和配置示例。

Enforce: Wiring Coverage into Hooks and CI

强制执行:将覆盖率接入钩子和CI

Pre-commit (composes with hk)

Pre-commit(与hk配合使用)

If using the hk skill, add coverage test steps to
hk.pkl
:
pkl
["test-unit"] {
  check = "scripts/quiet-on-success.sh pnpm test:unit:coverage"
}
["test-int"] {
  check = "scripts/quiet-on-success.sh pnpm test:int:coverage"
  depends = List("test-unit")
}
Key principles:
  • Coverage thresholds live in the test config, not in hook config
  • E2E tests are too slow for pre-commit — run in CI or manually
  • Order tiers by speed: unit first (fastest fail), then integration, then components
  • Wrap in quiet-on-success so passing tests produce no output
如果使用hk skill,将覆盖率测试步骤添加到
hk.pkl
pkl
["test-unit"] {
  check = "scripts/quiet-on-success.sh pnpm test:unit:coverage"
}
["test-int"] {
  check = "scripts/quiet-on-success.sh pnpm test:int:coverage"
  depends = List("test-unit")
}
核心原则:
  • 覆盖率阈值存放在测试配置中,而非钩子配置中
  • E2E测试运行过慢,不适合pre-commit阶段执行——放在CI中运行或手动执行
  • 按速度排序层级:单元测试优先(最快反馈失败),然后是集成测试,再是组件测试
  • 使用quiet-on-success封装,测试通过时不产生输出

CI

CI

Run all tiers with coverage in CI. Upload per-tier reports separately for visibility.
yaml
- name: Unit tests
  run: pnpm test:unit:coverage
- name: Integration tests
  run: pnpm test:int:coverage
- name: E2E tests
  run: pnpm test:e2e
在CI中运行所有层级的覆盖率测试,分别上传各层级的报告以便查看。
yaml
- name: Unit tests
  run: pnpm test:unit:coverage
- name: Integration tests
  run: pnpm test:int:coverage
- name: E2E tests
  run: pnpm test:e2e

Ratcheting

阈值逐步提升

For projects not yet at target:
  1. Measure current coverage
  2. Set threshold at current level
  3. After each improvement, bump the threshold
  4. Never lower it
See enforcement for detailed CI patterns, PR checks, and ratcheting workflow.
对尚未达到目标覆盖率的项目:
  1. 测量当前覆盖率
  2. 将阈值设置为当前水平
  3. 每次改进后,提升阈值
  4. 永远不要降低阈值
查阅强制执行获取详细的CI模式、PR检查和阈值逐步提升工作流。

Write Tests for New Code

为新代码编写测试

When adding features to a codebase with established coverage:
  1. Identify the tier: What kind of code are you writing? Match to the classification table above
  2. Write tests first (TDD): Test the expected behaviour before implementing
  3. Run coverage locally:
    --coverage
    for the relevant tier
  4. Handle exclusions: If code genuinely cannot be tested at this tier, document why and ensure coverage exists at another tier
  5. Verify thresholds pass: Pre-commit hooks catch regressions, but check early
在已经建立覆盖率体系的代码库中新增功能时:
  1. 识别测试层级:你编写的是哪类代码?匹配上文的分类表
  2. 测试先行(TDD):在实现功能前先测试预期行为
  3. 本地运行覆盖率统计:为对应层级添加
    --coverage
    参数运行
  4. 处理排除项:如果代码确实无法在当前层级测试,记录原因并确保在另一层级有覆盖
  5. 验证阈值通过:pre-commit钩子会捕获回归,但可以提前检查

Cross-tier exclusion pattern

跨层级排除模式

Every exclusion at one tier names the tier that provides coverage:
typescript
// Unit config excludes:
// Cross-tier: Service layer - requires database runtime - tested via integration tests
"src/domain/**/service.ts",

// Integration config excludes:
// Cross-tier: React components - requires browser context - tested via component + E2E tests
"src/components/**",
See coverage exclusions for the full exclusion taxonomy and documentation format.
每个层级的排除项都要注明提供覆盖的对应层级:
typescript
// Unit config excludes:
// Cross-tier: Service layer - requires database runtime - tested via integration tests
"src/domain/**/service.ts",

// Integration config excludes:
// Cross-tier: React components - requires browser context - tested via component + E2E tests
"src/components/**",
查阅覆盖率排除获取完整的排除分类和文档格式。

Test Organisation Patterns

测试组织模式

Directory structure

目录结构

text
tests/
  unit/          *.unit.spec.ts       Pure functions, domain logic
  int/           *.int.spec.ts        Database, API, access control
  components/    *.browser.spec.tsx   Rendered UI in real browser
  e2e/           *.e2e.spec.ts        Full user flows
  fixtures/      index.ts             Shared test data factories
  setup/         Per-tier setup files (DB init, browser cleanup)
text
tests/
  unit/          *.unit.spec.ts       Pure functions, domain logic
  int/           *.int.spec.ts        Database, API, access control
  components/    *.browser.spec.tsx   Rendered UI in real browser
  e2e/           *.e2e.spec.ts        Full user flows
  fixtures/      index.ts             Shared test data factories
  setup/         Per-tier setup files (DB init, browser cleanup)

Naming conventions

命名规范

Suffix encodes the tier — config
include
patterns use these suffixes for zero-ambiguity matching:
TierSuffixExample
Unit
.unit.spec.ts
slugify.unit.spec.ts
Integration
.int.spec.ts
films.int.spec.ts
Component
.browser.spec.tsx
FilmCard.browser.spec.tsx
E2E
.e2e.spec.ts
auth.e2e.spec.ts
后缀表明测试层级——配置中的
include
规则使用这些后缀实现无歧义匹配:
层级后缀示例
单元测试
.unit.spec.ts
slugify.unit.spec.ts
集成测试
.int.spec.ts
films.int.spec.ts
组件测试
.browser.spec.tsx
FilmCard.browser.spec.tsx
E2E测试
.e2e.spec.ts
auth.e2e.spec.ts

Test data factories

测试数据工厂

Use factory functions with auto-incrementing counters for unique identifiers:
typescript
let counter = 0
function createTestUser(overrides = {}) {
  counter++
  return {
    email: `test-${counter}@example.com`,
    name: `Test User ${counter}`,
    ...overrides,
  }
}
Counter-based (not random) for deterministic debugging. Reset between test runs if needed.
使用带自动递增计数器的工厂函数生成唯一标识符:
typescript
let counter = 0
function createTestUser(overrides = {}) {
  counter++
  return {
    email: `test-${counter}@example.com`,
    name: `Test User ${counter}`,
    ...overrides,
  }
}
基于计数器(而非随机数)实现确定性调试,必要时可在测试运行之间重置计数器。

Mock boundaries

Mock边界

  • Do mock: External APIs, third-party SDKs, environment-specific runtimes
  • Do not mock: Code you own — test through the public API
  • Database: Use a real local database for integration tests (SQLite, test containers)
  • Browser: Use a real browser for component tests (Playwright, Vitest browser mode)
  • Server-side imports: Stub server-only modules when testing in browser context
  • 应当Mock:外部API、第三方SDK、环境特定运行时
  • 不应当Mock:自有代码——通过公共API测试
  • 数据库:集成测试使用真实的本地数据库(SQLite、测试容器)
  • 浏览器:组件测试使用真实浏览器(Playwright、Vitest浏览器模式)
  • 服务端导入:在浏览器上下文测试时存根仅服务端可用的模块

Coverage Providers: Quick Reference

覆盖率工具:快速参考

ProviderEnvironmentWhen to useLimitations
v8Node.jsUnit, integration testsNot supported in browser mode
IstanbulBrowserComponent testsIgnore comments may not survive bundling
c8Node.js CLIStandalone v8 wrapperAlternative to built-in coverage
coverage.pyPythonAll tiers via pytest-covRequires source mapping for packages
go coverGoBuilt-in, all tiersPer-package profiles need merging
tarpaulinRustCargo integrationMay miss some async code paths
llvm-covRustHigher accuracyRequires nightly or specific toolchain
lcovAnyMerging multi-tier reportsFormat standard, not a provider
工具适用环境使用场景局限性
v8Node.js单元测试、集成测试不支持浏览器模式
Istanbul浏览器组件测试忽略注释可能在打包过程中丢失
c8Node.js CLI独立v8封装内置覆盖率工具的替代方案
coverage.pyPython配合pytest-cov覆盖所有层级包需要源码映射
go coverGo内置工具,覆盖所有层级需合并包级别的覆盖率报告
tarpaulinRustCargo集成可能遗漏部分异步代码路径
llvm-covRust更高准确率需要nightly或特定工具链
lcov任意合并多层级报告格式标准,本身不是覆盖率工具

Gotchas

常见问题

IssueFix
v8 undercounts arrow functionsLower
functions
threshold or restructure code
Istanbul ignore comments stripped by bundlerUse file-level exclusions in config instead
Concurrent DB writes in integration testsDisable parallelism, use single worker
Coverage directories conflict across tiersSeparate
reportsDirectory
per tier config
E2E tests too slow for pre-commitRun in CI only; document in project README
Ignore comment used without justificationAlways add a reason after the ignore directive
Coverage passes but tests are meaninglessReview test quality, not just the metric
New file added with no testsThreshold regression catches it at commit time
Browser tests import server-only codeCreate stub modules, alias in browser config
Flaky tests in pre-commit hooksInvestigate root cause; do not retry or skip
问题解决方案
v8少统计箭头函数覆盖率降低
functions
阈值或重构代码
Istanbul忽略注释被打包器移除在配置中使用文件级排除替代
集成测试中并发数据库写入冲突禁用并行执行,使用单worker
多层级之间覆盖率目录冲突为每个层级配置独立的
reportsDirectory
E2E测试运行过慢不适合pre-commit仅在CI中运行,在项目README中说明
使用忽略注释未说明理由忽略指令后必须添加原因
覆盖率通过但测试无实际意义审核测试质量,而非仅看指标
新增文件没有测试提交时阈值回归检查会捕获该问题
浏览器测试导入仅服务端可用的代码创建存根模块,在浏览器配置中设置别名
Pre-commit钩子中测试结果不稳定排查根本原因,不要重试或跳过

References

参考资料

  • Ecosystem Patterns — Index of per-language references:
    • TypeScript/JS | Python | Go | Rust | Merging
  • Coverage Exclusions — How to document and justify every exclusion
  • Enforcement — Wiring coverage into hk hooks, CI pipelines, and PR checks
  • 生态体系模式 —— 各语言参考索引:
    • TypeScript/JS | Python | Go | Rust | 报告合并
  • 覆盖率排除 —— 如何记录并说明每个排除项的合理性
  • 强制执行 —— 将覆盖率接入hk钩子、CI流水线和PR检查