Vitest アーキテクチャ概観:Vite ベースのテストフレームワークはどう構成されているか
前提知識
- ›Vite の基本的な理解(dev サーバー、プラグイン、モジュール変換)
- ›npm/pnpm ワークスペースとモノレポの概念に慣れていること
- ›TypeScript の実務的な知識
Vitest アーキテクチャ概観:Vite ベースのテストフレームワークはどう構成されているか
Vitest は「たまたま Vite を使っているテストランナー」ではありません。dev サーバーをフルテストプラットフォームへと変換する Vite プラグインそのものです。この根本的な設計判断を理解すると、コードベース全体の見え方が変わります。なぜ設定が Vite のプラグインフックを通じて流れるのか、なぜワークスペースの各プロジェクトが独自の ViteDevServer インスタンスを持つのか、なぜテストファイルがアプリケーションコードと同じ変換パイプラインで処理されるのか、すべての答えがここにあります。
この記事では全体の地図を描きます。モノレポの 17 パッケージを概観し、Node オーケストレーション層とワーカーランタイムの境界線を追い、中心となる Vitest クラスを解剖し、テスト実行時とプログラマティック用途を明確に分けるデュアル API 設計を理解していきましょう。
モノレポ構造とパッケージマップ
Vitest は pnpm ワークスペースとして構成されており、パッケージは packages/ 配下に置かれ、ドキュメント・サンプル・結合テスト用のディレクトリが別途用意されています。ワークスペースの定義はシンプルです。
packages:
- docs
- packages/*
- examples/*
- test/*
全パッケージの一覧は以下のとおりです。
| パッケージ | 役割 |
|---|---|
vitest |
コアフレームワーク:CLI、設定、オーケストレーション、プール、レポーター、ランタイムワーカー |
@vitest/runner |
フレームワーク非依存のテストランナー:DSL(describe/test/it)、収集、実行、フック、フィクスチャ |
@vitest/expect |
Chai をベースに Jest 互換マッチャーを加えたアサーションライブラリ |
@vitest/spy |
tinyspy をベースにしたモック/スパイシステム(vi.fn()、vi.spyOn()) |
@vitest/snapshot |
スナップショットテスト:インラインおよびファイルベースのスナップショット |
@vitest/mocker |
モジュールモックの基盤(vi.mock()、vi.hoisted()) |
@vitest/utils |
共有ユーティリティ:エラー処理、ソースマップ、シリアライズ、差分 |
@vitest/pretty-format |
スナップショットと差分のための値シリアライザ(Jest からフォーク) |
@vitest/browser |
ブラウザテストのオーケストレーション |
@vitest/browser-playwright |
Playwright ブラウザプロバイダー |
@vitest/browser-webdriverio |
WebDriverIO ブラウザプロバイダー |
@vitest/browser-preview |
ブラウザプレビュー UI コンポーネント |
@vitest/coverage-v8 |
V8 コードカバレッジプロバイダー |
@vitest/coverage-istanbul |
Istanbul コードカバレッジプロバイダー |
@vitest/ui |
Vue ベースのダッシュボード UI |
@vitest/web-worker |
Node テスト環境向け Web Worker ポリフィル |
@vitest/ws-client |
UI 通信用 WebSocket クライアント |
graph TD
subgraph "Core"
V[vitest]
R["@vitest/runner"]
E["@vitest/expect"]
S["@vitest/spy"]
SN["@vitest/snapshot"]
M["@vitest/mocker"]
U["@vitest/utils"]
PF["@vitest/pretty-format"]
end
subgraph "Browser"
B["@vitest/browser"]
BP["@vitest/browser-playwright"]
BW["@vitest/browser-webdriverio"]
BPR["@vitest/browser-preview"]
end
subgraph "Coverage"
CV8["@vitest/coverage-v8"]
CIS["@vitest/coverage-istanbul"]
end
subgraph "UI & Tools"
UI["@vitest/ui"]
WW["@vitest/web-worker"]
WS["@vitest/ws-client"]
end
V --> R
V --> E
V --> S
V --> SN
V --> M
V --> U
E --> U
E --> PF
SN --> PF
B --> V
UI --> WS
ここで重要なのは、厳格な階層構造です。@vitest/runner は vitest への依存を一切持たず、テスト DSL と実行エンジンをフレームワーク非依存の形で定義しています。vitest パッケージはこれらのサブパッケージを合成し、Vite のモジュール変換パイプラインと Node.js ワーカー管理を組み合わせて動作させます。
ヒント: コードベースを読み始めるなら
packages/vitest/src/public/が最適な出発点です。ここには明示的な API 境界が集まっており、任意のユースケースに関係する内部モジュールをすぐに把握できます。
2 つの実行ドメイン:Node とランタイム
Vitest のアーキテクチャで最も重要な境界線は、2 つの実行ドメインの分離です。
-
Node ドメイン(
packages/vitest/src/node/)— すべてを統括するメインプロセス。CLI パース、設定解決、Vite サーバー管理、プール生成、レポーター呼び出し、ファイル監視、WebSocket API を担います。 -
ランタイムドメイン(
packages/vitest/src/runtime/)— テストファイルを実行するワーカースレッドまたは子プロセス。環境セットアップ、Vite の変換パイプラインを通じたモジュール読み込み、テスト収集、テスト実行を担います。
flowchart LR
subgraph "Node Domain (Main Process)"
CLI[CLI / Programmatic API]
Core[Vitest Class]
Config[Config Resolution]
Pool[Pool Manager]
Reporters[Reporters]
State[StateManager]
end
subgraph "Runtime Domain (Workers)"
Worker[Worker Entry]
Env[Environment Setup]
Runner[Test Runner]
Modules[Module Loading via Vite]
end
CLI --> Core
Core --> Config
Core --> Pool
Core --> Reporters
Core --> State
Pool -- "birpc over MessagePort" --> Worker
Worker --> Env
Worker --> Runner
Runner --> Modules
Modules -- "RPC fetch()" --> Core
Runner -- "RPC events" --> State
この分離は、隔離と並列実行のために存在します。テストファイルは jsdom や happy-dom などグローバル状態を操作する環境を使う場合があります。それぞれを別ワーカーで動かすことで干渉を防いでいます。2 つのドメイン間の通信ブリッジは birpc ——MessagePort(スレッド)または process.send(フォーク)上に透過的な関数呼び出しインターフェースを構築する双方向 RPC ライブラリです。
Vitest クラス——中央オーケストレーター
packages/vitest/src/node/core.ts の Vitest クラスは、フレームワーク全体の重心です。主要なサブシステムをすべて所有しています。
classDiagram
class Vitest {
+version: string
+logger: Logger
+projects: TestProject[]
+watcher: VitestWatcher
+vcs: VCSProvider
-pool: ProcessPool
-_vite: ViteDevServer
-_state: StateManager
-_cache: VitestCache
-_snapshot: SnapshotManager
-_testRun: TestRun
+config: ResolvedConfig
+vite: ViteDevServer
+state: StateManager
+snapshot: SnapshotManager
+cache: VitestCache
+_setServer(options, server)
+start(cliFilters)
+close()
}
Vitest --> ViteDevServer
Vitest --> "1..*" TestProject
Vitest --> ProcessPool
Vitest --> StateManager
Vitest --> SnapshotManager
Vitest --> VitestCache
Vitest --> TestRun
Vitest --> VitestWatcher
コンストラクターは意図的に軽量に設計されており、Logger、VitestSpecifications、VitestWatcher の生成だけを行います。実際の初期化は Vite の configureServer フックが発火した後に呼ばれる _setServer() で行われます。
packages/vitest/src/node/core.ts#L207-L231
このメソッドは設定を解決し、StateManager・VitestCache・SnapshotManager・TestRun を生成したうえで、モジュールランナーと VCS プロバイダーをセットアップします。この設計により、設定ファイルの変更時に Vitest を再初期化できます——_setServer() は再構築の前にすべての状態を丁寧にリセットします。
マルチプロジェクトワークスペースアーキテクチャ
Vitest は複数プロジェクトの並列実行をサポートしており、各プロジェクトは独自の設定・Vite サーバー・モジュールリゾルバーを持ちます。packages/vitest/src/node/project.ts#L45-L93 の TestProject クラスが、単一のワークスペースプロジェクトを表します。
flowchart TD
V[Vitest] --> P1[TestProject 'unit']
V --> P2[TestProject 'integration']
V --> P3[TestProject 'e2e-chromium']
P1 --> VS1[ViteDevServer]
P1 --> C1[ResolvedConfig]
P1 --> R1[VitestResolver]
P2 --> VS2[ViteDevServer]
P2 --> C2[ResolvedConfig]
P2 --> R2[VitestResolver]
P3 --> VS3[ViteDevServer]
P3 --> C3[ResolvedConfig]
P3 --> R3[VitestResolver]
各 TestProject は独自の ViteDevServer インスタンスを持つため、プロジェクトごとに異なる Vite プラグイン・エイリアス・解決設定を使えます。packages/vitest/src/node/projects/resolveProjects.ts#L28-L34 のプロジェクト解決ロジックは、3 種類の定義スタイルに対応しています。
- 設定ファイルパス — glob で解決される
vitest.config.tsまたはvite.config.ts - ディレクトリパス — 設定ファイルをスキャンするディレクトリ
- インラインオブジェクト — ワークスペース定義内に直接記述する設定オブジェクト
プロジェクト名はユニークである必要があり、返却前にシステムが検証します。ブラウザプロジェクトは特別な扱いを受けます——browser.instances が設定された単一プロジェクトは、ブラウザごとに複数の子プロジェクトを生成します。
ビルドエントリとデュアル API 設計
rollup.config.js を見ると、慎重に設計されたデュアル API が見えてきます。
flowchart TD
subgraph "Test-time API (vitest)"
IDX["src/public/index.ts"]
IDX --> describe & test & it & expect & vi
end
subgraph "Programmatic Node API (vitest/node)"
NODE["src/public/node.ts"]
NODE --> createVitest & startVitest & reporters & config_types
end
subgraph "Worker Entries"
WT["workers/threads"]
WF["workers/forks"]
WVM["workers/vmThreads"]
WVF["workers/vmForks"]
end
subgraph "Other Entries"
CLI["cli"]
COV["coverage"]
SNAP["snapshot"]
end
テスト実行時 API(src/public/index.ts)は、テストファイルに必要なもの——describe、test、it、expect、vi、bench、フック、型ユーティリティ——をすべて再エクスポートします。@vitest/runner、@vitest/expect、および vitest 統合モジュールからインポートされます。
プログラマティック Node API(src/public/node.ts)は、Vitest をプログラムから操作するために必要なもの——createVitest、startVitest、レポータークラス、プールワーカー、設定型、シーケンサー——をエクスポートします。IDE 拡張やカスタムツールが使う API です。
この分離は意図的なものです。テストファイルが Node 側のオーケストレーションコードをインポートすべきではなく、ビルドツールがテストグローバルをインポートすべきでもありません。ワーカーエントリはパフォーマンスのために個別にバンドルされており、workers/threads をインポートしてもフレームワーク全体が読み込まれることはありません。
Vite をバックボーンとして
Vitest は Vite を利用するスタンドアロンツールではなく、それ自体が Vite プラグインの配列です。packages/vitest/src/node/plugins/index.ts#L26-L291 の VitestPlugin 関数は、Vite をテストランナーへと変換するプラグインの配列を返します。
flowchart TD
VP["VitestPlugin()"] --> Core["vitest (core plugin)"]
VP --> ME["MetaEnvReplacerPlugin"]
VP --> CSS["CSSEnablerPlugin"]
VP --> COV["CoverageTransform"]
VP --> RES["VitestCoreResolver"]
VP --> MOCK["MocksPlugins"]
VP --> OPT["VitestOptimizer"]
VP --> NORM["NormalizeURLPlugin"]
VP --> MRT["ModuleRunnerTransform"]
Core -- "config()" --> MergeDefaults["Merge configDefaults with user config"]
Core -- "configResolved()" --> SetupVitest["Store config, set env variables"]
Core -- "configureServer()" --> Init["vitest._setServer()"]
コアの vitest プラグインは 3 つの Vite フックを使います。
config()— Vitest のconfigDefaultsとユーザー設定をマージし、サーバーオプション(HMR の無効化、API ポートの設定)をセットアップし、esbuild/oxc のターゲットを調整します。configResolved()— すべてのプラグインが実行された後に最終的な設定マージを行い、UI プラグインの注入を処理し、解決済みの設定を Vite 設定オブジェクトに保存します。configureServer()— Vite サーバーの準備が整った後、vitest._setServer()を呼び出して初期化を完了させます。
サブプラグインはそれぞれ専門的な役割を担います。CoverageTransform はカバレッジのためにコードをインストルメント化します。MocksPlugins は変換レベルで vi.mock() 呼び出しをインターセプトし、VitestOptimizer は依存関係の最適化を管理します。MetaEnvReplacerPlugin は実行時の再代入を可能にするため import.meta.env を process.env に置き換えます。
ヒント: 設定の問題をデバッグするなら、
plugins/index.tsの 44 行目にあるconfig()フックが最初の調査地点です。Vitest のデフォルト値とユーザー設定が最初にマージされる場所であり、解決済み設定の最終的な値の出どころはここにあります。
次のステップ
アーキテクチャ全体の地図が描けたので、次は実際の実行パスを追いましょう。次の記事では、vitest run コマンドがバイナリエントリから始まり、CLI パース、設定ファイルの探索、Vite サーバーの生成、プラグインフックのライフサイクル全体、ワークスペースの解決、ワーカーへの設定シリアライズまでをたどります。このブートシーケンスを理解しておくと、設定の問題のデバッグや、Vitest のプログラマティック API を活用したカスタムツールの構築がはるかにスムーズになります。