Vitest完全ガイド2026:Next.js・React・Node.jsの高速テスト環境を構築する
なぜVitestか:Jestからの移行が進む理由
| Jest | Vitest | |
|---|---|---|
| 速度 | 遅い(特にTS変換) | Jestの5〜10倍高速 |
| TypeScript | 設定が複雑 | ゼロ設定で動作 |
| ESM | 混在問題あり | ESMネイティブ対応 |
| Vite統合 | なし | 設定を共有できる |
セットアップ
npm install -D vitest @vitest/coverage-v8 @testing-library/react @testing-library/jest-dom jsdom
npm install -D @testing-library/user-event msw
// vitest.config.ts
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
import tsconfigPaths from 'vite-tsconfig-paths';
export default defineConfig({
plugins: [react(), tsconfigPaths()],
test: {
environment: 'jsdom',
globals: true,
setupFiles: ['./src/test/setup.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'html', 'lcov'],
thresholds: {
lines: 80,
functions: 80,
branches: 70,
},
},
},
});
// src/test/setup.ts
import '@testing-library/jest-dom';
ユニットテストの基礎
// src/lib/utils.test.ts
import { describe, it, expect } from 'vitest';
import { formatCurrency, calculateTax } from './utils';
describe('formatCurrency', () => {
it('正の数値を日本円でフォーマットする', () => {
expect(formatCurrency(1234.56)).toBe('¥1,234.56');
});
it('ゼロを正しくフォーマットする', () => {
expect(formatCurrency(0)).toBe('¥0.00');
});
});
describe('calculateTax', () => {
it('標準税率10%を計算する', () => {
const { tax, total } = calculateTax(1000, 'standard');
expect(tax).toBe(100);
expect(total).toBe(1100);
});
});
Reactコンポーネントのテスト
// src/components/TodoItem.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, it, expect, vi } from 'vitest';
import { TodoItem } from './TodoItem';
const mockTodo = { id: '1', title: '牛乳を買う', completed: false };
describe('TodoItem', () => {
it('タイトルを表示する', () => {
render(<TodoItem todo={mockTodo} onToggle={vi.fn()} onDelete={vi.fn()} />);
expect(screen.getByText('牛乳を買う')).toBeInTheDocument();
});
it('チェックボックスをクリックするとonToggleが呼ばれる', async () => {
const onToggle = vi.fn();
render(<TodoItem todo={mockTodo} onToggle={onToggle} onDelete={vi.fn()} />);
await userEvent.click(screen.getByRole('checkbox'));
expect(onToggle).toHaveBeenCalledWith('1');
expect(onToggle).toHaveBeenCalledTimes(1);
});
});
APIのモック:MSWを使ったネットワークモック
// src/test/mocks/handlers.ts
import { http, HttpResponse } from 'msw';
export const handlers = [
http.get('/api/todos', () => {
return HttpResponse.json([
{ id: '1', title: 'Todo 1', completed: false },
{ id: '2', title: 'Todo 2', completed: true },
]);
}),
http.post('/api/todos', async ({ request }) => {
const body = await request.json() as any;
return HttpResponse.json({ id: '3', ...body }, { status: 201 });
}),
http.delete('/api/todos/:id', ({ params }) => {
if (params.id === 'invalid') {
return new HttpResponse(null, { status: 404 });
}
return new HttpResponse(null, { status: 204 });
}),
];
// src/test/setup.ts(MSW統合)
import '@testing-library/jest-dom';
import { setupServer } from 'msw/node';
import { handlers } from './mocks/handlers';
import { afterAll, afterEach, beforeAll } from 'vitest';
const server = setupServer(...handlers);
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
Server Actions のテスト(Next.js 15)
// src/app/actions/todo.test.ts
import { describe, it, expect, vi } from 'vitest';
import { createTodo } from './todo';
vi.mock('@/lib/db', () => ({
db: {
insert: vi.fn().mockReturnThis(),
values: vi.fn().mockResolvedValue([
{ id: '123', title: 'Test Todo', completed: false }
]),
},
}));
vi.mock('next/cache', () => ({ revalidatePath: vi.fn() }));
describe('createTodo', () => {
it('有効なフォームデータでTodoを作成できる', async () => {
const formData = new FormData();
formData.append('title', 'テストTodo');
const result = await createTodo({}, formData);
expect(result.message).toBe('作成しました!');
});
it('空のタイトルはバリデーションエラーになる', async () => {
const formData = new FormData();
formData.append('title', '');
const result = await createTodo({}, formData);
expect(result.errors.title).toBeDefined();
});
});
テストの実行
npm test # 通常の実行
npm test -- --watch # ウォッチモード
npm test -- --ui # UIモード(ブラウザで確認)
npm test -- --coverage # カバレッジ計測
高度なモックパターン
実務で頻繁に使うモックパターンを紹介します。
モジュール全体のモック
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { getUserProfile } from './userService';
// モジュール全体をモック
vi.mock('./api/client', () => ({
apiClient: { get: vi.fn(), post: vi.fn() },
}));
import { apiClient } from './api/client';
describe('getUserProfile', () => {
beforeEach(() => vi.clearAllMocks());
it('APIレスポンスを整形して返す', async () => {
vi.mocked(apiClient.get).mockResolvedValue({
data: { id: 1, name: '田中太郎', role: 'admin' },
});
const profile = await getUserProfile(1);
expect(profile).toEqual({ id: 1, displayName: '田中太郎', isAdmin: true });
});
it('API失敗時にnullを返す', async () => {
vi.mocked(apiClient.get).mockRejectedValue(new Error('Network Error'));
const profile = await getUserProfile(1);
expect(profile).toBeNull();
});
});
CI/CDへの統合
Vitestをチームの開発フローに組み込むことで、コードの品質を継続的に保てます。
GitHub Actionsでの設定
# .github/workflows/test.yml
name: Test
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: 'npm'
- run: npm ci
- run: npm test -- --reporter=verbose
- run: npm test -- --coverage
pre-commitフックでテストを実行
# .husky/pre-commit
#!/bin/sh
npx vitest run --changed HEAD~1
変更されたファイルに関連するテストだけを実行することで、コミット前の待ち時間を最小限に抑えられます。
カバレッジ設定の最適化
カバレッジを正しく設定することで、テストの品質指標を可視化できます。
// vitest.config.ts(カバレッジ詳細設定)
export default defineConfig({
test: {
coverage: {
provider: 'v8',
reporter: ['text', 'html', 'lcov', 'json-summary'],
reportsDirectory: './coverage',
thresholds: {
lines: 80,
functions: 80,
branches: 70,
statements: 80,
perFile: true,
},
include: ['src/**/*.{ts,tsx}'],
exclude: [
'src/**/*.test.{ts,tsx}',
'src/**/*.stories.{ts,tsx}',
'src/types/**',
'src/test/**',
],
},
},
});
カバレッジレポートはHTMLで出力すると、ファイルごとの未テスト行を視覚的に確認できます。open coverage/index.html でブラウザから確認しましょう。
まとめ
Vitestを選ぶ理由:
- TypeScriptがゼロ設定で動く
- ESMネイティブ対応
- Jestの5〜10倍高速
- UIモードで視覚的に確認できる
テスト品質のチェックポイント:
- カバレッジ80%以上
- ハッピーパスだけでなくエラーケースも
- 実装ではなく振る舞いをテスト
- MSWでAPIをモック(実際のAPIは叩かない)