Read OSS

レポーター、カバレッジ、Vitest の拡張:出力とプラグインレイヤー

上級

前提知識

  • 第 1〜4 回:Vitest のアーキテクチャ、起動、実行、ランナーの完全な理解
  • コードカバレッジの基本的な概念への慣れ
  • WebSocket 通信の基礎知識(UI セクションの理解に必要)

レポーター、カバレッジ、Vitest の拡張:出力とプラグインレイヤー

前回までの 4 つの記事で追ってきた一連の処理(設定の解決、プール管理、ワーカーの起動、テストの収集と実行)は、最終的にすべて「出力」のために存在しています。カラー付きのターミナル出力や JSON ファイル、JUnit XML レポート、カバレッジサマリー、リアルタイムのブラウザダッシュボードなどを生成するレポーターと出力システムこそが、Vitest の内部状態を開発者の目に見える形へ変換する場所です。

この最終回では以下のトピックを取り上げます。Reporter インターフェースとそのライフサイクルフック、レポートされるタスクオブジェクトの階層、組み込みレポーターのカタログ、カバレッジプロバイダーシステム、テストの実行順序戦略について解説します。さらに、Vitest UI を支える WebSocket API、プログラマティックな Node API、そして expectspysnapshot を独立して組み合わせ可能にするサブパッケージアーキテクチャも扱います。

Reporter インターフェースとライフサイクルフック

packages/vitest/src/node/types/reporter.tsReporter インターフェースは、レポーターが購読できるすべてのイベントを定義しています。

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-L65BaseReporter を継承しており、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.tsBaseSequencer は、賢いデフォルト順序を実装しています。

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 を別途依存関係に加える必要がありません。

Tips: VS Code 拡張の開発では、createVitestreporters オプションを渡してカスタムレポーターを注入し、テスト結果を VS Code の Test API にマッピングするのが定石です。TestSpecification クラスを使えば、個別のファイルや行番号を指定した特定のテストだけを実行することもできます。

サブパッケージアーキテクチャ:expect、spy、snapshot

アサーション、モック、スナップショットの各システムは独立したパッケージとして実装されており、それらが組み合わさって Vitest のテスト API を構成しています。

@vitest/expectpackages/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 は toBetoEqualtoHaveBeenCalledtoMatchSnapshot といったおなじみのマッチャーをすべて追加します。JestExtend はカスタムマッチャー用の expect.extend() を有効にします。GLOBAL_EXPECT 定数はグローバルスコープに現在の expect インスタンスを保持するために使われ、TestRunner がテストごとにスコープ付きの expect を設定できるようにします。

@vitest/spypackages/spy/src/index.ts)は tinyspy を基盤にモック関数を提供します。createMockInstance() 関数は mockImplementation()mockReturnValue()mockResolvedValue() など充実した API を持つ Mock オブジェクトを返します。グローバルな MOCK_RESTORE セットがすべてのモックを追跡し、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/ がスナップショットアサーションをランナーのライフサイクルに接続します。

シリーズのまとめ

5 回にわたって、Vitest の完全なアーキテクチャを追ってきました。

  1. アーキテクチャ — Node オーケストレーションとワーカーランタイムをクリーンに分離した 17 パッケージのモノレポ
  2. 起動シーケンス — バイナリエントリから CLI パース、設定の探索、Vite サーバーの生成、plugin フックまで
  3. プールシステム — birpc 通信による Pool/PoolRunner/PoolWorker の 3 層設計
  4. ランナー — フレームワーク非依存のテスト DSL、収集・実行エンジン、フックシステム、フィクスチャ
  5. 出力レイヤー — レポーター、カバレッジ、シーケンサー、WebSocket UI API、組み合わせ可能なサブパッケージ

ここから浮かび上がる設計思想は「層状のコンポジション」です。@vitest/runner は Vite について何も知りません。@vitest/expect はテスト実行について何も知りません。vitest パッケージがこれらすべてを組み合わせ、Vite の開発サーバーを変換のバックボーンとして、birpc を通信ブリッジとして活用しています。このアーキテクチャがシステムをテスト可能で拡張しやすいものにしており、一度各層を理解してしまえば、このスコープのフレームワークとしては驚くほど見通しよく読み進められる構造になっています。