レポーター、カバレッジ、Vitest の拡張:出力とプラグインレイヤー
前提知識
- ›第 1〜4 回:Vitest のアーキテクチャ、起動、実行、ランナーの完全な理解
- ›コードカバレッジの基本的な概念への慣れ
- ›WebSocket 通信の基礎知識(UI セクションの理解に必要)
レポーター、カバレッジ、Vitest の拡張:出力とプラグインレイヤー
前回までの 4 つの記事で追ってきた一連の処理(設定の解決、プール管理、ワーカーの起動、テストの収集と実行)は、最終的にすべて「出力」のために存在しています。カラー付きのターミナル出力や JSON ファイル、JUnit XML レポート、カバレッジサマリー、リアルタイムのブラウザダッシュボードなどを生成するレポーターと出力システムこそが、Vitest の内部状態を開発者の目に見える形へ変換する場所です。
この最終回では以下のトピックを取り上げます。Reporter インターフェースとそのライフサイクルフック、レポートされるタスクオブジェクトの階層、組み込みレポーターのカタログ、カバレッジプロバイダーシステム、テストの実行順序戦略について解説します。さらに、Vitest UI を支える WebSocket API、プログラマティックな Node API、そして expect・spy・snapshot を独立して組み合わせ可能にするサブパッケージアーキテクチャも扱います。
Reporter インターフェースとライフサイクルフック
packages/vitest/src/node/types/reporter.ts の 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)
このライフサイクルは細かい粒度で設計されています。すべてのフックの実装は任意であり、必要なものだけを選んで実装できます。onTestRunStart はテスト実行前に仕様の全リストを受け取ります。onTestModuleQueued はファイルがワーカーに送られた直後、まだロードされる前に発火します。onTestModuleCollected はファイル内のテストが探索された後に発火します。個々のテスト結果は onTestCaseResult を通じて届きます。
TestRunEndReason は 'passed'、'failed'、'interrupted' のいずれかを取り、最後の値はユーザーによるキャンセルやバイルアウトを示します。
ReportedTask の階層
レポーターはランナーのタスクをそのまま受け取るわけではなく、情報が豊富なラッパーオブジェクトを通じて操作します。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 は個々のテストをラップします。それぞれが結果、診断情報(実行時間、リトライ回数、ヒープ使用量)、ナビゲーション(親・子要素)への型付きアクセサーを提供しています。
モジュールやスイートが持つ TestCollection はイテラブルで、for...of で子要素を走査できます。state() メソッドは計算済みの状態を返し、値は 'pending'、'queued'、'running'、'passed'、'failed'、'skipped' のいずれかです。
組み込みレポーター
Vitest には 12 種類のレポーターが同梱されており、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,
}
| レポーター | 出力形式 | 主な用途 |
|---|---|---|
default |
進捗付きのカラーターミナル | インタラクティブな開発 |
verbose |
全テストの一覧 | 詳細出力が必要な CI |
dot |
テストごとにドット 1 個 | 最小限の CI 出力 |
tree |
ツリー構造の出力 | 視覚的な階層確認 |
json |
JSON ファイル | 機械処理向け |
junit |
JUnit XML | CI システム連携 |
tap / tap-flat |
TAP プロトコル | TAP 対応ツールとの統合 |
github-actions |
GitHub アノテーション | PR との統合 |
blob |
バイナリ blob | シャーディング/マージワークフロー |
agent |
AI エージェント向け構造化出力 | 自動化ツール |
hanging-process |
プロセス診断情報 | スタックしたテストのデバッグ |
ほとんどのレポーターは packages/vitest/src/node/reporters/base.ts#L43-L65 の BaseReporter を継承しており、TTY 検出、サイレントモード、エラーフォーマット、サマリー描画、バナー表示といった共通機能が提供されています。
Tips: 複数のレポーターを同時に使用できます。
--reporter=default --reporter=json --outputFile=results.jsonのように指定すると、defaultレポーターは標準出力に書き出しながら、jsonレポーターはファイルに書き出します。カスタムレポーターはモジュールパスで指定することも可能です(例:--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 クラスはしきい値チェック、ファイルのグロブマッチング、レポート生成といった共通機能を提供します。V8 プロバイダーは V8 の組み込みカバレッジ機能を使用するため高速ですが、ソースマップが絡むコードでは精度が落ちることがあります。Istanbul はソースレベルでコードを計装するため、変換オーバーヘッドが生じる代わりにより信頼性の高いカバレッジが得られます。
CoverageTransform という Vite plugin がソースマップの再マッピングを担当し、カバレッジの位置情報が変換後の出力ではなく元のソースを指すように調整します。カバレッジデータはワーカーごとに収集され(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 を別途依存関係に加える必要がありません。
Tips: VS Code 拡張の開発では、
createVitestにreportersオプションを渡してカスタムレポーターを注入し、テスト結果を VS Code の Test API にマッピングするのが定石です。TestSpecificationクラスを使えば、個別のファイルや行番号を指定した特定のテストだけを実行することもできます。
サブパッケージアーキテクチャ:expect、spy、snapshot
アサーション、モック、スナップショットの各システムは独立したパッケージとして実装されており、それらが組み合わさって Vitest のテスト API を構成しています。
@vitest/expect(packages/expect/src/index.ts)は Chai をベースに、Jest 互換のマッチャーを追加しています。
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 plugin は toBe、toEqual、toHaveBeenCalled、toMatchSnapshot といったおなじみのマッチャーをすべて追加します。JestExtend はカスタムマッチャー用の expect.extend() を有効にします。GLOBAL_EXPECT 定数はグローバルスコープに現在の expect インスタンスを保持するために使われ、TestRunner がテストごとにスコープ付きの expect を設定できるようにします。
@vitest/spy(packages/spy/src/index.ts)は tinyspy を基盤にモック関数を提供します。createMockInstance() 関数は mockImplementation()、mockReturnValue()、mockResolvedValue() など充実した API を持つ Mock オブジェクトを返します。グローバルな MOCK_RESTORE セットがすべてのモックを追跡し、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/ がスナップショットアサーションをランナーのライフサイクルに接続します。
シリーズのまとめ
5 回にわたって、Vitest の完全なアーキテクチャを追ってきました。
- アーキテクチャ — Node オーケストレーションとワーカーランタイムをクリーンに分離した 17 パッケージのモノレポ
- 起動シーケンス — バイナリエントリから CLI パース、設定の探索、Vite サーバーの生成、plugin フックまで
- プールシステム — birpc 通信による
Pool/PoolRunner/PoolWorkerの 3 層設計 - ランナー — フレームワーク非依存のテスト DSL、収集・実行エンジン、フックシステム、フィクスチャ
- 出力レイヤー — レポーター、カバレッジ、シーケンサー、WebSocket UI API、組み合わせ可能なサブパッケージ
ここから浮かび上がる設計思想は「層状のコンポジション」です。@vitest/runner は Vite について何も知りません。@vitest/expect はテスト実行について何も知りません。vitest パッケージがこれらすべてを組み合わせ、Vite の開発サーバーを変換のバックボーンとして、birpc を通信ブリッジとして活用しています。このアーキテクチャがシステムをテスト可能で拡張しやすいものにしており、一度各層を理解してしまえば、このスコープのフレームワークとしては驚くほど見通しよく読み進められる構造になっています。