test-coverage
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseTest 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.
| Tier | Tests | Cost to write | Cost to run |
|---|---|---|---|
| Unit | Pure functions, domain logic, validation, parsing | Low | Milliseconds |
| Integration | Database queries, API boundaries, access control, service interactions | Medium | Seconds |
| Component | Rendered UI in a real browser, user interactions, visual states | Medium | Seconds |
| E2E | Full user flows across the entire stack | High | Minutes |
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: , , , , , . See ecosystem patterns for tool recommendations per language.
package.jsongo.modCargo.tomlpyproject.tomlsetup.py*.csproj检查项目标识文件:、、、、、。查阅生态体系模式获取各语言的工具推荐。
package.jsongo.modCargo.tomlpyproject.tomlsetup.py*.csproj2. 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 pattern matching only its source files
include - 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/int3. 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
undefinedExample: 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
undefinedRun 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:
| Question | If yes | If no |
|---|---|---|
| Business logic or domain rules? | Unit tests (highest priority) | Continue |
| Access control or authorisation? | Integration tests | Continue |
| Data validation or parsing? | Unit tests | Continue |
| API endpoint or mutation? | Integration tests | Continue |
| UI component with logic? | Component tests | Continue |
| Full user flow? | E2E tests | Continue |
| Can it run in the test environment? | Write tests | Document exclusion |
| Auto-generated code? | Exclude with comment | Write tests |
| Thin wrapper around tested library? | Consider excluding | Write tests |
对每个未覆盖的文件或函数,询问以下问题:
| 问题 | 是 | 否 |
|---|---|---|
| 业务逻辑或领域规则? | 单元测试(最高优先级) | 继续判断 |
| 访问控制或鉴权? | 集成测试 | 继续判断 |
| 数据校验或解析? | 单元测试 | 继续判断 |
| API端点或变更操作? | 集成测试 | 继续判断 |
| 带逻辑的UI组件? | 组件测试 | 继续判断 |
| 完整用户流程? | E2E测试 | 继续判断 |
| 能否在测试环境中运行? | 编写测试 | 记录为排除项 |
| 自动生成的代码? | 加注释排除 | 编写测试 |
| 已测试库的薄封装? | 考虑排除 | 编写测试 |
Phase 3: Prioritise
阶段3:优先级排序
Triage order (highest value first):
- Domain logic and business rules (unit)
- Access control and authorisation (integration)
- Data validation and input parsing (unit)
- API endpoints and mutations (integration)
- UI components with conditional logic (component)
- Async/server-rendered components (E2E)
- Configuration and wiring (tested implicitly by higher tiers)
处理顺序(价值从高到低):
- 领域逻辑和业务规则(单元测试)
- 访问控制和鉴权(集成测试)
- 数据校验和输入解析(单元测试)
- API端点和变更操作(集成测试)
- 带条件逻辑的UI组件(组件测试)
- 异步/服务端渲染组件(E2E测试)
- 配置和装配代码(由更高层级的测试隐式覆盖)
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.pklpkl
["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.pklpkl
["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:e2eRatcheting
阈值逐步提升
For projects not yet at target:
- Measure current coverage
- Set threshold at current level
- After each improvement, bump the threshold
- Never lower it
See enforcement for detailed CI patterns, PR checks, and ratcheting workflow.
对尚未达到目标覆盖率的项目:
- 测量当前覆盖率
- 将阈值设置为当前水平
- 每次改进后,提升阈值
- 永远不要降低阈值
查阅强制执行获取详细的CI模式、PR检查和阈值逐步提升工作流。
Write Tests for New Code
为新代码编写测试
When adding features to a codebase with established coverage:
- Identify the tier: What kind of code are you writing? Match to the classification table above
- Write tests first (TDD): Test the expected behaviour before implementing
- Run coverage locally: for the relevant tier
--coverage - Handle exclusions: If code genuinely cannot be tested at this tier, document why and ensure coverage exists at another tier
- Verify thresholds pass: Pre-commit hooks catch regressions, but check early
在已经建立覆盖率体系的代码库中新增功能时:
- 识别测试层级:你编写的是哪类代码?匹配上文的分类表
- 测试先行(TDD):在实现功能前先测试预期行为
- 本地运行覆盖率统计:为对应层级添加参数运行
--coverage - 处理排除项:如果代码确实无法在当前层级测试,记录原因并确保在另一层级有覆盖
- 验证阈值通过: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 patterns use these suffixes for zero-ambiguity matching:
include| Tier | Suffix | Example |
|---|---|---|
| Unit | | |
| Integration | | |
| Component | | |
| E2E | | |
后缀表明测试层级——配置中的规则使用这些后缀实现无歧义匹配:
include| 层级 | 后缀 | 示例 |
|---|---|---|
| 单元测试 | | |
| 集成测试 | | |
| 组件测试 | | |
| E2E测试 | | |
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
覆盖率工具:快速参考
| Provider | Environment | When to use | Limitations |
|---|---|---|---|
| v8 | Node.js | Unit, integration tests | Not supported in browser mode |
| Istanbul | Browser | Component tests | Ignore comments may not survive bundling |
| c8 | Node.js CLI | Standalone v8 wrapper | Alternative to built-in coverage |
| coverage.py | Python | All tiers via pytest-cov | Requires source mapping for packages |
| go cover | Go | Built-in, all tiers | Per-package profiles need merging |
| tarpaulin | Rust | Cargo integration | May miss some async code paths |
| llvm-cov | Rust | Higher accuracy | Requires nightly or specific toolchain |
| lcov | Any | Merging multi-tier reports | Format standard, not a provider |
| 工具 | 适用环境 | 使用场景 | 局限性 |
|---|---|---|---|
| v8 | Node.js | 单元测试、集成测试 | 不支持浏览器模式 |
| Istanbul | 浏览器 | 组件测试 | 忽略注释可能在打包过程中丢失 |
| c8 | Node.js CLI | 独立v8封装 | 内置覆盖率工具的替代方案 |
| coverage.py | Python | 配合pytest-cov覆盖所有层级 | 包需要源码映射 |
| go cover | Go | 内置工具,覆盖所有层级 | 需合并包级别的覆盖率报告 |
| tarpaulin | Rust | Cargo集成 | 可能遗漏部分异步代码路径 |
| llvm-cov | Rust | 更高准确率 | 需要nightly或特定工具链 |
| lcov | 任意 | 合并多层级报告 | 格式标准,本身不是覆盖率工具 |
Gotchas
常见问题
| Issue | Fix |
|---|---|
| v8 undercounts arrow functions | Lower |
| Istanbul ignore comments stripped by bundler | Use file-level exclusions in config instead |
| Concurrent DB writes in integration tests | Disable parallelism, use single worker |
| Coverage directories conflict across tiers | Separate |
| E2E tests too slow for pre-commit | Run in CI only; document in project README |
| Ignore comment used without justification | Always add a reason after the ignore directive |
| Coverage passes but tests are meaningless | Review test quality, not just the metric |
| New file added with no tests | Threshold regression catches it at commit time |
| Browser tests import server-only code | Create stub modules, alias in browser config |
| Flaky tests in pre-commit hooks | Investigate root cause; do not retry or skip |
| 问题 | 解决方案 |
|---|---|
| v8少统计箭头函数覆盖率 | 降低 |
| Istanbul忽略注释被打包器移除 | 在配置中使用文件级排除替代 |
| 集成测试中并发数据库写入冲突 | 禁用并行执行,使用单worker |
| 多层级之间覆盖率目录冲突 | 为每个层级配置独立的 |
| 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检查