Reporter、覆盖率与扩展 Vitest:输出与插件层
前置知识
- ›第 1-4 篇:全面理解 Vitest 架构、启动流程、执行机制与 runner
- ›具备基本的代码覆盖率概念
- ›了解 WebSocket 通信机制(UI 章节需要)
Reporter、覆盖率与扩展 Vitest:输出与插件层
前四篇文章追踪了 Vitest 的完整内部流程——配置解析、pool 管理、worker 启动、测试收集与执行。这一切工作最终都指向同一个目的:产出结果。无论是带颜色的终端输出、JSON 文件、JUnit XML 报告、覆盖率摘要,还是实时浏览器仪表盘,reporter 和输出系统都是 Vitest 内部状态对外呈现的窗口。
本篇作为系列终章,将涵盖以下内容:Reporter 接口及其生命周期钩子、报告任务对象的层级结构、内置 reporter 目录、覆盖率提供器系统、测试排序策略、驱动 Vitest UI 的 WebSocket API、编程式 Node API,以及让 expect、spy、snapshot 可独立组合的子包架构。
Reporter 接口与生命周期钩子
packages/vitest/src/node/types/reporter.ts 中的 Reporter 接口定义了 reporter 可以监听的所有事件:
sequenceDiagram
participant V as Vitest
participant TR as TestRun
participant R as Reporter
V->>R: onInit(vitest)
Note over TR: Test run begins
TR->>R: onTestRunStart(specifications)
loop For each test module
TR->>R: onTestModuleQueued(testModule)
TR->>R: onTestModuleCollected(testModule)
TR->>R: onTestModuleStart(testModule)
loop For each test
TR->>R: onTestCaseReady(testCase)
TR->>R: onTestCaseResult(testCase)
end
loop For each suite
TR->>R: onTestSuiteReady(testSuite)
TR->>R: onTestSuiteResult(testSuite)
end
TR->>R: onTestModuleEnd(testModule)
end
TR->>R: onTestRunEnd(testModules, errors, reason)
Note over R: Optional events
R-->>R: onUserConsoleLog(log)
R-->>R: onHookStart(hook) / onHookEnd(hook)
R-->>R: onTestCaseAnnotate(testCase, annotation)
R-->>R: onCoverage(coverage)
这套生命周期的设计粒度很细。reporter 不需要实现每一个钩子——所有钩子都是可选的。onTestRunStart 在任何测试执行之前就会收到完整的 specification 列表。onTestModuleQueued 在文件被发送给 worker 但尚未加载时触发。onTestModuleCollected 在文件中的测试被发现后触发。单个测试结果通过 onTestCaseResult 送达。
TestRunEndReason 的取值为 'passed'、'failed' 或 'interrupted',最后一个表示用户主动取消或触发了 bail-out。
ReportedTask 层级结构
Reporter 接收的不是原始 runner task,而是经过封装的富对象。packages/vitest/src/node/reporters/reported-tasks.ts 中的层级结构提供了清晰的 API:
classDiagram
class ReportedTaskImplementation {
+task: RunnerTask
+project: TestProject
+id: string
+location: LocationInfo
+ok(): boolean
+meta(): TaskMeta
}
class TestModule {
+type: "module"
+moduleId: string
+children: TestCollection
+state(): TestModuleState
+diagnostic(): ModuleDiagnostic
}
class TestSuite {
+type: "suite"
+name: string
+parent: TestSuite | TestModule
+children: TestCollection
+state(): TestSuiteState
}
class TestCase {
+type: "test"
+name: string
+fullName(): string
+parent: TestSuite | TestModule
+result(): TestResult
+diagnostic(): TestDiagnostic
+annotations(): TestAnnotation[]
}
ReportedTaskImplementation <|-- TestModule
ReportedTaskImplementation <|-- TestSuite
ReportedTaskImplementation <|-- TestCase
TestModule 代表一个测试文件(即 @vitest/runner 中所称的 File)。TestSuite 对应 describe 块。TestCase 对应单个测试。每个对象都提供了类型化的访问器,用于读取结果、诊断信息(耗时、重试次数、堆内存使用)以及层级导航(parent、children)。
模块和 suite 上的 TestCollection 是可迭代的,支持用 for...of 遍历子元素。state() 方法返回计算后的状态:'pending'、'queued'、'running'、'passed'、'failed' 或 'skipped'。
内置 Reporter
Vitest 内置了 12 个 reporter,注册于 packages/vitest/src/node/reporters/index.ts#L49-L62:
export const ReportersMap = {
'default': DefaultReporter,
'agent': AgentReporter,
'blob': BlobReporter,
'verbose': VerboseReporter,
'dot': DotReporter,
'json': JsonReporter,
'tap': TapReporter,
'tap-flat': TapFlatReporter,
'junit': JUnitReporter,
'tree': TreeReporter,
'hanging-process': HangingProcessReporter,
'github-actions': GithubActionsReporter,
}
| Reporter | 输出格式 | 适用场景 |
|---|---|---|
default |
带进度的彩色终端 | 日常开发 |
verbose |
逐条列出所有测试 | 需要详细输出的 CI |
dot |
每个测试一个点 | CI 极简输出 |
tree |
树形结构输出 | 直观展示层级 |
json |
JSON 文件 | 机器消费 |
junit |
JUnit XML | CI 系统集成 |
tap / tap-flat |
TAP 协议 | TAP 消费方 |
github-actions |
GitHub 注释 | PR 集成 |
blob |
二进制 blob | 分片/合并工作流 |
agent |
结构化输出供 AI agent 使用 | 自动化工具 |
hanging-process |
进程诊断信息 | 调试卡住的测试 |
大多数 reporter 继承自 packages/vitest/src/node/reporters/base.ts#L43-L65 中的 BaseReporter,它提供了公共功能:TTY 检测、静默模式、错误格式化、摘要渲染和 banner 显示。
提示: 可以同时使用多个 reporter:
--reporter=default --reporter=json --outputFile=results.json。defaultreporter 写入标准输出,json写入文件。自定义 reporter 可以通过模块路径指定:--reporter=./my-reporter.ts。
覆盖率提供器系统
Vitest 的覆盖率系统是可插拔的。packages/vitest/src/node/coverage.ts 中的提供器系统按需加载对应的模块:
flowchart TD
Config["coverage.provider: 'v8' | 'istanbul' | 'custom'"] --> Resolve["resolveCoverageProviderModule()"]
Resolve --> V8["@vitest/coverage-v8"]
Resolve --> Istanbul["@vitest/coverage-istanbul"]
Resolve --> Custom["Custom module"]
V8 & Istanbul & Custom --> Provider["CoverageProvider interface"]
Provider --> Init["initialize(ctx)"]
subgraph "Test Execution"
Init --> StartWorker["startCoverageInsideWorker()"]
StartWorker --> Tests["Tests run"]
Tests --> StopWorker["stopCoverageInsideWorker()"]
end
StopWorker --> Report["generateCoverage()"]
Report --> Transform["CoverageTransform plugin<br>(source map remapping)"]
Transform --> Output["Coverage reports<br>(text, html, json, clover)"]
BaseCoverageProvider 类提供了共享功能:阈值检查、文件 glob 匹配、报告生成。V8 提供器使用 V8 引擎内置的覆盖率功能,速度快,但对 source map 代码有时精度略逊。Istanbul 在源码层面进行插桩,覆盖率更可靠,代价是引入了额外的转换开销。
CoverageTransform 这个 Vite plugin 负责处理 source map 的重映射,确保覆盖率数据指向原始源码位置,而非转换后的产物。覆盖率数据在每个 worker 中独立收集(通过 runBaseTests 中调用的 startCoverageInsideWorker/stopCoverageInsideWorker),最终在 Node 侧合并。
测试排序器
测试执行顺序由排序器控制。packages/vitest/src/node/sequencers/BaseSequencer.ts 中的 BaseSequencer 实现了一套智能的默认排序策略:
flowchart TD
Sort["BaseSequencer.sort(files)"] --> GroupOrder["1. sequence.groupOrder"]
GroupOrder --> ProjectName["2. Project name (alphabetical)"]
ProjectName --> Isolation["3. Isolated files first"]
Isolation --> Cache{Has cached results?}
Cache -- "no" --> Size["Sort by file size (larger first)"]
Cache -- "yes" --> Failed{Previously failed?}
Failed -- "yes" --> First["Run first"]
Failed -- "no" --> Duration["Sort by duration (longer first)"]
这套排序策略优先执行失败的测试(快速反馈),其次执行耗时较长的测试(优先启动开销大的测试,从而缩短整体墙上时间)。shard() 方法使用 SHA-1 哈希,以确定性方式将测试分发到各个分片。
自定义排序器可以继承 BaseSequencer 并覆盖 sort() 方法——例如,优先执行与变更文件相关的测试,或实现自定义的分组策略。
WebSocket API 与 Vitest UI
Vitest UI 通过 WebSocket 服务器与测试进程通信,该服务器在 packages/vitest/src/api/setup.ts 中建立:
sequenceDiagram
participant UI as Vitest UI (Browser)
participant WS as WebSocket Server
participant V as Vitest Process
UI->>WS: Connect to /__vitest_api__
WS->>V: createBirpc(handlers, events)
Note over UI,V: Bidirectional RPC established
UI->>V: getFiles()
V-->>UI: File list with test results
UI->>V: rerun(files)
V->>V: Schedule test run
V->>UI: onTaskUpdate(packs, events)
V->>UI: onFinished(files, errors)
V->>UI: onUserConsoleLog(log)
UI->>V: getModuleGraph(id)
V-->>UI: Module dependency graph
服务器将 /__vitest_api__ 路径上的 HTTP 连接升级为 WebSocket,并在验证 API 配置后建立 birpc 通道。WebSocketHandlers 接口暴露了 getFiles()、getTransformResult()、getModuleGraph()、rerun() 等方法。事件则通过 WebSocketEvents 接口反向流向客户端,其结构与 Reporter 生命周期一一对应。
@vitest/ws-client 包提供浏览器端客户端,@vitest/ui 是一个 Vue 应用,通过消费这套 API 来渲染仪表盘,实时展示测试结果、模块依赖图和错误详情。
编程式 Node API
从 packages/vitest/src/public/node.ts 导出的公开 Node API,专为 IDE 插件、自定义构建工具和编程式测试执行场景而设计:
// Core functions
export { startVitest } from '../node/cli/cli-api'
export { createVitest } from '../node/create'
export { VitestPlugin } from '../node/plugins'
// Reporter infrastructure
export { ReportersMap, DefaultReporter, ... } from '../node/reporters'
// Pool workers (for custom pools)
export { ThreadsPoolWorker, ForksPoolWorker, ... } from '../node/pools/workers/...'
// Sequencer
export { BaseSequencer } from '../node/sequencers/BaseSequencer'
// Types
export type { Vitest, Reporter, TestProject, TestSpecification, ... }
createVitest 与 startVitest 的区别很关键:createVitest 创建并初始化 Vitest 实例,但不执行测试;startVitest 则在创建实例的同时运行测试。对于 IDE 集成场景,通常应选择 createVitest,以便自主控制测试的执行时机和范围。
此外,该 API 还重新导出了 Vite 的工具函数:createViteServer、parseAst 以及版本信息——在构建 Vitest 工具时无需单独依赖 Vite。
提示: 开发 VS Code 插件时,可通过
createVitest配合reporters选项注入自定义 reporter,将测试结果映射到 VS Code 的 Test API。TestSpecification类支持按文件或按行号运行指定测试。
子包架构:expect、spy、snapshot
断言、mock 和快照系统以独立包的形式实现,并组合进 Vitest 的测试 API。
@vitest/expect(packages/expect/src/index.ts)以 Chai 为基础,添加了与 Jest 兼容的 matcher:
graph TD
subgraph "@vitest/expect"
Chai["chai (base)"]
JCE["JestChaiExpect<br>(toBe, toEqual, toThrow...)"]
JAM["JestAsymmetricMatchers<br>(any, anything, objectContaining...)"]
JE["JestExtend<br>(expect.extend())"]
CM["customMatchers registry"]
Chai --> JCE
Chai --> JAM
JCE --> JE
JE --> CM
end
subgraph "@vitest/spy"
Spy["tinyspy (base)"]
MockFn["createMockInstance()"]
MockRestore["Mock lifecycle<br>(clear, reset, restore)"]
Spy --> MockFn
MockFn --> MockRestore
end
subgraph "@vitest/snapshot"
Client["SnapshotClient"]
State["SnapshotState"]
Plugins["Serializer plugins"]
Client --> State
State --> Plugins
end
JestChaiExpect 插件添加了所有常用 matcher(toBe、toEqual、toHaveBeenCalled、toMatchSnapshot)。JestExtend 支持通过 expect.extend() 添加自定义 matcher。GLOBAL_EXPECT 常量用于在全局作用域中存储当前的 expect 实例,使 TestRunner 能够为每个测试配置独立的 expect 作用域。
@vitest/spy(packages/spy/src/index.ts)基于 tinyspy 提供 mock 函数。createMockInstance() 返回一个具有完整 API 的 Mock 对象:mockImplementation()、mockReturnValue()、mockResolvedValue() 等。全局的 MOCK_RESTORE 集合记录所有 mock,支持通过 vi.restoreAllMocks() 批量还原。
@vitest/snapshot(packages/snapshot/src/index.ts)同时处理文件快照和内联快照。SnapshotClient 管理断言流程,SnapshotState 按文件追踪快照数据,自定义序列化器可通过 addSerializer() 注册。快照环境支持插件化——默认实现将 .snap 文件写入磁盘,也可以自定义环境将快照存储到数据库等其他介质。
这些包由 Vitest 的集成层统一消费:src/integrations/chai/ 负责接入 expect,src/integrations/spy.ts 配置 spy 模块,src/integrations/snapshot/ 将快照断言与 runner 生命周期对接。
系列总结
经过五篇文章,我们完整追踪了 Vitest 的架构全貌:
- 架构概览 — 17 个包组成的 monorepo,Node 编排层与 worker 运行时之间清晰分离
- 启动流程 — 从二进制入口出发,经过 CLI 解析、配置发现、Vite server 创建到插件钩子
- Pool 系统 —
Pool/PoolRunner/PoolWorker三层设计,以 birpc 作为通信桥梁 - Runner — 与框架无关的测试 DSL、收集机制、执行引擎、hook 系统与 fixture
- 输出层 — Reporter、覆盖率、排序器、WebSocket UI API 以及可组合的子包
从中浮现的设计哲学是分层组合。@vitest/runner 对 Vite 一无所知。@vitest/expect 对测试执行毫不关心。vitest 包将所有这些部件组合在一起,以 Vite dev server 作为转换骨干,以 birpc 作为通信桥梁。正是这种架构,让整个系统具备了可测试性和可扩展性——一旦理解了各个层次,即便面对这样规模的框架,也能出人意料地轻松找到方向。