この記事の要点
• Vitest 3はBrowser Mode安定化とReporter API刷新が目玉
• Jest互換APIでViteプロジェクトのテストをゼロ設定で高速実行
• ワークスペース機能改善でエンタープライズ規模のモノレポにも対応
Vitest は Vite の変換パイプラインをそのまま利用する高速なユニットテストランナーです。ESM / TypeScript / JSX をゼロ設定で扱え、Jest 互換の API を提供します。Vitest 3 では Browser Mode の安定化、Reporter API の刷新、ワークスペース機能の改善が進み、エンタープライズ規模のモノレポでも安心して利用できる段階に達しました。
Vitest 3の概要
位置づけ
flowchart LR
Src["Source (TS/JSX)"] --> Vite["Vite Transform"]
Vite --> Runner["Vitest Runner"]
Runner --> Node["Node Env (jsdom / happy-dom)"]
Runner --> Browser["Browser Mode (Playwright/WebdriverIO)"]
Runner --> Report["Reporter API"]
Report --> CI["CI / Dashboard"]
Jest と異なり、Vitest は Vite のビルドパイプラインを再利用するため、開発時に使っている alias / plugin / env 設定がそのままテストでも使えます。
主要な新機能
1. Browser Mode の安定化
Vitest 3 で Browser Mode が安定版となり、Playwright / WebdriverIO プロバイダをネイティブにサポートします。
// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
browser: {
enabled: true,
provider: 'playwright',
name: 'chromium',
headless: true,
screenshotFailures: true,
},
},
});
これにより、jsdom では再現できないレイアウト・スタイル・ブラウザAPI を実ブラウザ上で検証できます。
2. 新しい Reporter API
Reporter API が刷新され、テスト結果のライフサイクルを細かくフックできるようになりました。
// custom-reporter.ts
import type { Reporter, TestModule, TestCase } from 'vitest/node';
export default class CustomReporter implements Reporter {
onTestModuleCollected(module: TestModule) {
console.log(`Collected: ${module.moduleId}`);
}
onTestCaseResult(testCase: TestCase) {
const { name, result } = testCase;
console.log(`${result.state}: ${name}`);
}
onTestRunEnd(modules: readonly TestModule[]) {
const failed = modules
.flatMap(m => [...m.children])
.filter(t => t.result().state === 'failed');
console.log(`Failed: ${failed.length}`);
}
}
// vitest.config.ts
import CustomReporter from './custom-reporter';
export default defineConfig({
test: {
reporters: ['default', new CustomReporter()],
},
});
3. Workspaces の強化
モノレポ全体を 1 コマンドでテスト可能です。
// vitest.workspace.ts
import { defineWorkspace } from 'vitest/config';
export default defineWorkspace([
{
test: {
name: 'node',
environment: 'node',
include: ['packages/server/**/*.test.ts'],
},
},
{
test: {
name: 'jsdom',
environment: 'jsdom',
include: ['packages/ui/**/*.test.tsx'],
setupFiles: ['./packages/ui/test-setup.ts'],
},
},
{
test: {
name: 'browser',
include: ['packages/e2e/**/*.test.ts'],
browser: {
enabled: true,
provider: 'playwright',
name: 'chromium',
},
},
},
]);
4. 型レベルテスト
expectTypeOf と assertType を使って型の契約をテストできます。
import { describe, expectTypeOf, test } from 'vitest';
type User = { id: string; name: string };
function getUser(id: string): Promise<User> {
return Promise.resolve({ id, name: 'Alice' });
}
describe('type tests', () => {
test('getUser returns Promise<User>', () => {
expectTypeOf(getUser).parameter(0).toEqualTypeOf<string>();
expectTypeOf(getUser).returns.resolves.toMatchTypeOf<{ id: string }>();
});
});
vitest --typecheck
5. Snapshot の改善
インラインスナップショットの差分が読みやすくなり、シリアライザの拡張 API が整理されました。
import { expect, test } from 'vitest';
test('inline snapshot', () => {
const result = { status: 'ok', items: [1, 2, 3] };
expect(result).toMatchInlineSnapshot(`
{
"items": [1, 2, 3],
"status": "ok",
}
`);
});
実践サンプル
React コンポーネントのテスト
// Button.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, expect, it, vi } from 'vitest';
import { Button } from './Button';
describe('<Button />', () => {
it('renders children', () => {
render(<Button>Click</Button>);
expect(screen.getByRole('button', { name: 'Click' })).toBeInTheDocument();
});
it('calls onClick', async () => {
const user = userEvent.setup();
const onClick = vi.fn();
render(<Button onClick={onClick}>Save</Button>);
await user.click(screen.getByRole('button'));
expect(onClick).toHaveBeenCalledOnce();
});
});
// test-setup.ts
import '@testing-library/jest-dom/vitest';
import { cleanup } from '@testing-library/react';
import { afterEach } from 'vitest';
afterEach(() => {
cleanup();
});
API ハンドラのテスト (MSW)
// server.ts
import { setupServer } from 'msw/node';
import { http, HttpResponse } from 'msw';
export const server = setupServer(
http.get('/api/users/:id', ({ params }) => {
return HttpResponse.json({ id: params.id, name: 'Alice' });
}),
);
// users.test.ts
import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest';
import { server } from './server';
import { fetchUser } from './users';
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
describe('fetchUser', () => {
it('returns a user', async () => {
const user = await fetchUser('42');
expect(user).toEqual({ id: '42', name: 'Alice' });
});
});
Browser Mode でのレイアウトテスト
// layout.test.ts
import { page } from '@vitest/browser/context';
import { expect, test } from 'vitest';
test('responsive layout', async () => {
await page.viewport(1280, 800);
document.body.innerHTML = `
<div class="grid" style="display:grid;grid-template-columns:1fr 1fr;">
<div>A</div><div>B</div>
</div>
`;
const grid = document.querySelector('.grid')!;
const rect = grid.getBoundingClientRect();
expect(rect.width).toBeGreaterThan(0);
});
カバレッジ設定
export default defineConfig({
test: {
coverage: {
provider: 'v8',
reporter: ['text', 'lcov', 'html'],
include: ['src/**/*.{ts,tsx}'],
exclude: ['src/**/*.d.ts', 'src/**/*.test.*'],
thresholds: {
lines: 80,
functions: 80,
branches: 75,
statements: 80,
},
},
},
});
比較表
Vitest vs Jest
| 項目 | Vitest 3 | Jest 29 |
|---|---|---|
| ESM 対応 | ネイティブ | experimental |
| TypeScript | ゼロ設定 | ts-jest 必要 |
| Vite 設定の共有 | 可 | 不可 |
| Browser Mode | あり (Playwright) | なし |
| 型テスト | expectTypeOf | なし |
| 並列実行 | worker threads | child process |
ポイント: Vitest 3のBrowser Modeを使えば、実際のブラウザ環境でコンポーネントテストを実行でき、JSDOMの制約を回避できます。
ベストプラクティス
1. Vite 設定を共有する
vitest.config.ts は vite.config.ts を extends できるため、alias や plugin を二重管理しないようにします。
import { defineConfig, mergeConfig } from 'vitest/config';
import viteConfig from './vite.config';
export default mergeConfig(viteConfig, defineConfig({
test: {
environment: 'jsdom',
setupFiles: ['./test-setup.ts'],
},
}));
2. テストファイルを実装の近くに置く
src/components/Button.tsx と src/components/Button.test.tsx を並べる配置は、リファクタリング時に壊れにくく推奨されます。
3. vi.mock より依存注入
可能なら依存を引数で受け取る設計にし、モック量を減らします。
4. --shard で CI を並列化
vitest run --shard=1/4
vitest run --shard=2/4
vitest run --shard=3/4
vitest run --shard=4/4
5. expect.soft で複数アサーションの失敗を一度に表示
import { expect, test } from 'vitest';
test('user', () => {
const user = { name: 'Alice', age: 30, email: 'a@example.com' };
expect.soft(user.name).toBe('Alice');
expect.soft(user.age).toBe(30);
expect.soft(user.email).toContain('@');
});
実践メモ: vitest.config.tsでpoolをforks/threadsに設定すると、テストの並列実行��略を制御でき、大規��テストスイートの実行時間を短縮できます。
注意点
- Jest からの移行時、
jest.fn()はvi.fn()に置き換えが必要です。jest.mock→vi.mockも同様です。 - ESM で書かれたテストでは top-level
awaitが使えますが、CommonJS 依存との相互運用に注意が必要です。 - Browser Mode は Playwright / WebdriverIO のいずれかのランタイムが必要で、CI 環境にブラウザを用意する必要があります。
happy-domはjsdomより速い一方で API が一部異なるため、既存コードでは互換性を確認してから切り替えてください。- テスト並列度を上げすぎるとメモリ不足を引き起こすことがあります。
poolOptions.threads.maxThreadsで調整します。
注意: Vitest 2からの移行時はReporter APIの変更に注意が必要です。カスタムレポーターを使用している場合は対応が必要です。
導入手順
新規プロジェクト
pnpm add -D vitest @vitest/ui jsdom @testing-library/react @testing-library/jest-dom
// package.json
{
"scripts": {
"test": "vitest",
"test:run": "vitest run",
"test:ui": "vitest --ui",
"coverage": "vitest run --coverage"
}
}
// vitest.config.ts
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
setupFiles: ['./test-setup.ts'],
globals: true,
},
});
Jest からの移行
# 1. 依存の入れ替え
pnpm remove jest @types/jest ts-jest
pnpm add -D vitest
# 2. jest.config.js → vitest.config.ts
# 3. jest.* → vi.* への置換
# 4. scripts の更新
パフォーマンス
Vitest は Vite の開発サーバと同じ変換パイプラインを使うため、既に開発中であれば差分変換が極めて速くなります。--watch モードでは変更されたファイルに関連するテストのみが再実行され、フィードバックループが短縮されます。
並列度のチューニング
export default defineConfig({
test: {
pool: 'threads',
poolOptions: {
threads: {
singleThread: false,
maxThreads: 8,
minThreads: 2,
},
},
isolate: true,
},
});
isolate: false にするとモジュールキャッシュが共有されより高速ですが、グローバル状態を持つテストでは副作用に注意が必要です。
FAQ
Q: Jest のスナップショットは移行できますか? A: 多くのケースでそのまま使えますが、シリアライズ形式の差分で更新が必要になる場合があります。
Q: Storybook と併用できますか?
A: @storybook/addon-vitest (旧 test-runner) を使うと、Storyをテストとして実行できます。
Q: Node 環境と jsdom 環境を混在させたい
A: Workspaces もしくはファイル先頭に // @vitest-environment node コメントを置きます。
Q: globals: true は使うべき?
A: 小規模では便利ですが、明示的な import のほうが IDE のサポートが安定します。
Q: Browser Mode は CI で遅くなりませんか? A: Playwright の headless モードと並列実行を組み合わせれば実用的な速度が出ます。ただし通常のユニットテストは jsdom / happy-dom が無難です。
さらなる実装パターン
カスタムマッチャー
import { expect } from 'vitest';
expect.extend({
toBeValidEmail(received: string) {
const pass = /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(received);
return {
pass,
message: () => pass
? `expected ${received} not to be a valid email`
: `expected ${received} to be a valid email`,
};
},
});
declare module 'vitest' {
interface Assertion<T = any> {
toBeValidEmail(): T;
}
}
パラメタライズドテスト
import { describe, expect, test } from 'vitest';
describe.each([
{ a: 1, b: 2, expected: 3 },
{ a: -1, b: 1, expected: 0 },
{ a: 10, b: 20, expected: 30 },
])('add($a, $b)', ({ a, b, expected }) => {
test(`returns ${expected}`, () => {
expect(a + b).toBe(expected);
});
});
まとめ
Vitest 3 は、Vite エコシステムのユニットテストランナーとしてほぼ完成形に達したリリースです。Browser Mode の安定化により、jsdom では届かなかった領域までカバーできるようになり、Reporter API の刷新とワークスペース機能の改善はエンタープライズモノレポでの運用を大きく楽にします。Jest からの移行コストも比較的小さく、Vite ベースのプロジェクトであれば積極的に導入する価値があります。