Read OSS

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,以及让 expectspysnapshot 可独立组合的子包架构。

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.jsondefault reporter 写入标准输出,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, ... }

createViteststartVitest 的区别很关键:createVitest 创建并初始化 Vitest 实例,但不执行测试;startVitest 则在创建实例的同时运行测试。对于 IDE 集成场景,通常应选择 createVitest,以便自主控制测试的执行时机和范围。

此外,该 API 还重新导出了 Vite 的工具函数:createViteServerparseAst 以及版本信息——在构建 Vitest 工具时无需单独依赖 Vite。

提示: 开发 VS Code 插件时,可通过 createVitest 配合 reporters 选项注入自定义 reporter,将测试结果映射到 VS Code 的 Test API。TestSpecification 类支持按文件或按行号运行指定测试。

子包架构:expect、spy、snapshot

断言、mock 和快照系统以独立包的形式实现,并组合进 Vitest 的测试 API。

@vitest/expectpackages/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(toBetoEqualtoHaveBeenCalledtoMatchSnapshot)。JestExtend 支持通过 expect.extend() 添加自定义 matcher。GLOBAL_EXPECT 常量用于在全局作用域中存储当前的 expect 实例,使 TestRunner 能够为每个测试配置独立的 expect 作用域。

@vitest/spypackages/spy/src/index.ts)基于 tinyspy 提供 mock 函数。createMockInstance() 返回一个具有完整 API 的 Mock 对象:mockImplementation()mockReturnValue()mockResolvedValue() 等。全局的 MOCK_RESTORE 集合记录所有 mock,支持通过 vi.restoreAllMocks() 批量还原。

@vitest/snapshotpackages/snapshot/src/index.ts)同时处理文件快照和内联快照。SnapshotClient 管理断言流程,SnapshotState 按文件追踪快照数据,自定义序列化器可通过 addSerializer() 注册。快照环境支持插件化——默认实现将 .snap 文件写入磁盘,也可以自定义环境将快照存储到数据库等其他介质。

这些包由 Vitest 的集成层统一消费:src/integrations/chai/ 负责接入 expect,src/integrations/spy.ts 配置 spy 模块,src/integrations/snapshot/ 将快照断言与 runner 生命周期对接。

系列总结

经过五篇文章,我们完整追踪了 Vitest 的架构全貌:

  1. 架构概览 — 17 个包组成的 monorepo,Node 编排层与 worker 运行时之间清晰分离
  2. 启动流程 — 从二进制入口出发,经过 CLI 解析、配置发现、Vite server 创建到插件钩子
  3. Pool 系统Pool/PoolRunner/PoolWorker 三层设计,以 birpc 作为通信桥梁
  4. Runner — 与框架无关的测试 DSL、收集机制、执行引擎、hook 系统与 fixture
  5. 输出层 — Reporter、覆盖率、排序器、WebSocket UI API 以及可组合的子包

从中浮现的设计哲学是分层组合@vitest/runner 对 Vite 一无所知。@vitest/expect 对测试执行毫不关心。vitest 包将所有这些部件组合在一起,以 Vite dev server 作为转换骨干,以 birpc 作为通信桥梁。正是这种架构,让整个系统具备了可测试性和可扩展性——一旦理解了各个层次,即便面对这样规模的框架,也能出人意料地轻松找到方向。