Vitest ユニットテスト実践:設定から TDD 開発フローまで
はじめに
Jest で ESM プロジェクトを設定するのにどれくらい時間がかかりますか?ts-jest、babel-jest、jest.config.js… さらにモジュール解決の問題に対処する必要があります。私はかつて、Jest が .vue ファイルの import 文を正しく認識できるようにするために、丸一日を費やしたことがあります。
しかし、Vitest なら設定はたった一行で済みます。
これは大げさではありません。Vite プロジェクトで初めて vitest を実行したとき、テストケースが数秒で完了するのを見て、まるで 3 年間動いていた古いパソコンを新品に買い替えたような爽快感を覚えました。
Vitest はどれくらい速いのでしょうか?公式データとコミュニティの実測値によると、コールドスタートは約 200ms(Jest は 2-4 秒)、500 個のテストケースは約 8 秒で完了します(Jest は約 45 秒)。さらに嬉しいのは、Vite と設定を共有し、TypeScript をネイティブサポートし、API は Jest とほぼ同じであること——移行コストは 30 分程度で済みます。
この記事では、ゼロからの設定から完全な TDD フローまで、Mocking テクニックや Coverage 設定を含めて解説します。新規プロジェクトで Vitest を使いたい方も、既存プロジェクトを Jest から移行したい方も、必要な情報が見つかるはずです。
Vitest とは?なぜこれほど速いのか?
簡単に言えば、Vitest は Vite ネイティブのテストフレームワークです。
すでに Vite でプロジェクトを構築しているなら、Vitest は基本的に「すぐに使える」状態です。Vite の設定をそのまま再利用——エイリアス、環境変数、CSS 処理、すべて自動的に継承されます。Jest のような transform、moduleFileExtensions、moduleNameMapper を設定する必要がありません。
主な利点は 3 つあります:
速度。 コールドスタートは約 200 ミリ秒、Jest は通常 2-4 秒かかります。大規模プロジェクトでは差がより顕著になります:500 個のテストケースで、Vitest は約 8 秒、Jest は約 45 秒(DEV Community 2026 年の実測値)。この差は決して小さくありません。
Jest 互換性。 API はほぼ同じ——describe、it、expect、vi.fn()、書き方は Jest と変わりません。Jest からの移行は、基本的に import パスを変更するだけです。
スマートウォッチ。 「HMR for tests」と呼ばれる機能があります:コードを変更すると、関連するテストだけが再実行され、すべてが最初からやり直されるわけではありません。開発時の即座なフィードバックは、とても快適です。
「何か」を説明したところで、次はインストール方法を見ていきましょう。
インストールと設定
まず、システム要件:Vite バージョン 6.0.0 以上、Node バージョン 20.0.0 以上。比較的新しいプロジェクトであれば、これらは満たされているはずです。
インストール
コマンド一つ:
npm install -D vitest
これだけです。@types/jest、ts-jest、jest-environment-jsdom などをインストールする必要はありません。Vitest は TypeScript をネイティブサポートしています。
設定ファイル
2 つの方法があります:vite.config.ts に test フィールドを追加するか、別途 vitest.config.ts を作成するか。
プロジェクトがシンプルなら、直接 vite.config.ts を使用:
// vite.config.ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
globals: true, // グローバル変数、毎回 import { describe, it, expect } する必要なし
environment: 'node', // または 'jsdom'(ブラウザ環境のテスト)
include: ['tests/**/*.test.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'html', 'lcov'],
},
},
})
globals: true はとても便利です——有効にすると、describe、it、expect がすべてグローバル変数になり、各テストファイルで import する必要がなくなります。Jest と同じです。
テストに DOM API が必要な場合(コンポーネントのレンダリングをテストするなど)、environment を 'jsdom' に変更し、jsdom をインストール:
npm install -D jsdom
実行スクリプト
package.json に 2 行を追加:
{
"scripts": {
"test": "vitest",
"test:run": "vitest run"
}
}
npm test でウォッチモードに入り(コード変更で自動再実行)、npm run test:run で一回実行して終了(CI 環境で使用)。
設定はこれだけです。Jest のような preset、transform、moduleFileExtensions と比較すると、かなり楽になります。
ユニットテストの作成
テストファイルの命名規則は .test.ts または .spec.ts で、tests/ ディレクトリに配置するか、ソースファイルと同じレベルに配置します。チームの慣習に従います。
基本構造
最もシンプルなテスト:
import { describe, it, expect } from 'vitest'
import { add, divide } from './math'
describe('Math utilities', () => {
it('should add two numbers', () => {
expect(add(2, 3)).toBe(5)
})
it('should throw on division by zero', () => {
expect(() => divide(10, 0)).toThrow('Division by zero')
})
})
describe はテストのグループ化、it は個々のテストケースを定義、expect でアサーションを行います。
globals: true を有効にすれば、import 行は省略できます。
よく使うアサーション
最も使用頻度の高いもの:
// 基本的な等価性
expect(value).toBe(5) // 厳密な等価(===)
expect(obj).toEqual({ a: 1 }) // 深い等価性
// 真偽値の判定
expect(value).toBeTruthy()
expect(value).toBeFalsy()
expect(value).toBeNull()
// 例外
expect(() => fn()).toThrow()
expect(() => fn()).toThrow('Error message')
// 数値の比較
expect(n).toBeGreaterThan(10)
expect(n).toBeLessThanOrEqual(5)
// 配列/文字列の包含
expect(arr).toContain('item')
expect(str).toMatch(/pattern/)
テストのフィルタリング
開発時に特定のテストだけを実行したい?.only を使用:
it.only('このテストだけ実行', () => { ... })
一時的にテストをスキップしたい?.skip を使用:
it.skip('一時的にスキップ', () => { ... })
これらは describe にも使用できます:describe.only(...)、describe.skip(...)。
実行してみましょう。ウォッチモードでは、コードを変更するとすぐにテスト結果が緑または赤に変わります——この即座なフィードバックは、Jest のように毎回数秒待つ必要がある体験と比べて、遥かに優れています。
TDD 開発フローの実践
TDD(テスト駆動開発)の核となる理念はシンプルです:先にテストを書き、それからコードを書く。
直感に反するように聞こえるかもしれませんが、実際に使ってみると、コードを書く前に「この関数は何をすべきか」を明確に考えるよう強制されることがわかります。コードを書いてからテストを追加する場合——その時はテストが単なる「カバレッジを稼ぐ」形式主義になりがちです。
以下で、実践的なケースを使って完全なフローをデモンストレーションします。メールアドレス検証関数 validateEmail を実装します。
Step 1:テストを書く、まだ実装がない
まずテストファイルを作成:
// tests/validateEmail.test.ts
import { describe, it, expect } from 'vitest'
import { validateEmail } from '../src/validateEmail'
describe('validateEmail', () => {
it('should return true for valid email', () => {
expect(validateEmail('test@example.com')).toBe(true)
})
it('should return false for invalid email', () => {
expect(validateEmail('invalid')).toBe(false)
})
})
この時点では validateEmail 関数はまだ存在しないため、テストを実行するとエラーになります。しかし問題ありません——これが TDD の最初のステップです:テストを失敗させる。
Step 2:最小限のコードを実装
今、関数を作成し、テストを通すための最もシンプルな実装を書きます:
// src/validateEmail.ts
export function validateEmail(email: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
}
テストを実行:npm test。
両方のテストが緑になった?では、Step 2 は完了です。
Step 3:より多くの境界テストを追加
基本的なテストは通りましたが、メールアドレス検証にはまだ多くの境界ケースがあります。いくつか追加しましょう:
// tests/validateEmail.test.ts (追加)
it('should return false for empty string', () => {
expect(validateEmail('')).toBe(false)
})
it('should return false for email without domain', () => {
expect(validateEmail('user@')).toBe(false)
})
it('should return false for email with spaces', () => {
expect(validateEmail('test @example.com')).toBe(false)
})
テストを実行——すべて通れば、正規表現が適切に書かれています。失敗すれば、正規表現を修正します。
Step 4:リファクタリング
テストがすべて通ったので、安心してコードをリファクタリングできます。例えば、正規表現をより厳密なバージョンに変更したり、コメントを追加したり:
// src/validateEmail.ts
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
export function validateEmail(email: string): boolean {
if (!email || email.trim() === '') {
return false
}
return EMAIL_REGEX.test(email)
}
変更後、テストを再実行——依然としてすべて緑。これが TDD の利点です:リファクタリング時にテストがセーフティネットとなり、間違えればすぐにわかります。
なぜ TDD が使いやすいのか?
正直なところ、最初は「先にテストを書く」ことに慣れませんでした。しかし数回使ってみると気づきました:
- 明確に考えてから着手:テストを書くときは関数の振る舞いを設計しており、入力と出力を明確に考えることを強制されます。
- 迅速なイテレーション:Vitest のウォッチモードでは、コード変更が秒単位でフィードバックされ、待つ必要がありません。
- 安全なリファクタリング:テストカバレッジがあれば、コードを変更しても心配ありません。
簡単な関数から始めてみてください。例えばユーティリティ関数やフォーマット関数。慣れたら、複雑なロジックに拡張できます。
Mocking 高度テクニック
ユニットテストでは、多くの場合、外部依存を「偽装」する必要があります——API リクエスト、データベースクエリ、サードパーティライブラリ。このとき Mock を使用します。
Vitest は Jest と同様の Mock API を提供し、核となるのは vi オブジェクトです。
vi.fn():単一の関数をモック
最もシンプルな使い方、偽の関数を作成:
import { vi, describe, it, expect } from 'vitest'
describe('vi.fn() demo', () => {
it('tracks calls', () => {
const mockFn = vi.fn()
mockFn('hello')
mockFn('world')
expect(mockFn).toHaveBeenCalledTimes(2)
expect(mockFn).toHaveBeenNthCalledWith(1, 'hello')
})
})
戻り値を事前に設定することも可能:
const mockFn = vi.fn().mockReturnValue('mocked result')
// または非同期の戻り値
const asyncMock = vi.fn().mockResolvedValue({ data: 'ok' })
vi.mock():モジュール全体をモック
ある関数をテストしたいが、外部 API に依存している?そのモジュールを直接モック:
import { vi, describe, it, expect, beforeEach } from 'vitest'
import { fetchUser } from './api'
import { UserService } from './UserService'
// api モジュールをモック
vi.mock('./api', () => ({
fetchUser: vi.fn().mockResolvedValue({ id: 1, name: 'Alice' })
}))
describe('UserService', () => {
beforeEach(() => {
vi.clearAllMocks() // 各テストの前に以前の呼び出し記録をクリア
})
it('should fetch user', async () => {
const service = new UserService()
const user = await service.getUser(1)
expect(user.name).toBe('Alice')
expect(fetchUser).toHaveBeenCalledWith(1)
})
})
vi.mock() はモジュールのインポート前に実行されるため、ファイルの先頭に配置します。
vi.spyOn():実際の関数を監視
関数を完全に置き換えるのではなく、「監視」したい場合:
import { vi, describe, it, expect, afterEach } from 'vitest'
import { calculator } from './calculator'
describe('spyOn demo', () => {
afterEach(() => {
vi.restoreAllMocks()
})
it('tracks add calls', () => {
const addSpy = vi.spyOn(calculator, 'add')
const result = calculator.add(2, 3)
expect(result).toBe(5) // 元の関数は通常通り実行
expect(addSpy).toHaveBeenCalledWith(2, 3) // 同時に呼び出しを記録
})
})
spyOn は mock よりも穏やか——関数は通常通り動作し、「モニター」が追加されるだけです。
グローバルオブジェクトのモック
テスト時に実際の HTTP リクエストを送信したくない?グローバル fetch をモック:
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve({ data: 'mocked' })
}))
または、Vitest の vi.stubGlobal を使用して window、localStorage などのブラウザグローバルオブジェクトをモック。
Mock はテストの中で最もトリッキーな部分です。まずシンプルな vi.fn() から始め、慣れてから vi.mock() に進むことをお勧めします。各テスト後に Mock の状態をクリアすることを忘れないでください(clearAllMocks または restoreAllMocks)。そうしないと、異なるテスト間で互いに汚染されます。
Coverage とベストプラクティス
テストカバレッジはコード品質の参考指標の一つです。Vitest は 2 つの Coverage provider をサポート:v8(より高速、ネイティブサポート)と istanbul(互換性が高い)。通常は v8 で十分です。
Coverage の設定
vite.config.ts に追加:
test: {
coverage: {
provider: 'v8',
reporter: ['text', 'html', 'lcov'], // 出力形式
thresholds: {
lines: 80, // 行カバレッジ閾値
functions: 80, // 関数カバレッジ閾値
branches: 70, // 分岐カバレッジ閾値
},
exclude: ['node_modules/', 'tests/', '**/*.d.ts'],
},
}
実行コマンド:
vitest run --coverage
ターミナルにカバレッジレポートが表示され、同時に coverage/ ディレクトリが生成され、中に HTML レポートがあり、どのコードがカバーされていないかを確認できます。
閾値を設定する意味
thresholds は飾りではありません——カバレッジが設定値に達しない場合、Vitest はエラーで終了します。これは CI 環境で非常に有用:コードが一定のテスト品質に達することを強制し、「適当なコミット」を防ぎます。
もちろん、閾値は高すぎないように。80% が合理的な出発点で、100% は逆にチームを疲れさせる原因になります。
CI/CD 統合
GitHub Actions または他の CI にステップを追加:
# .github/workflows/test.yml
- name: Run tests with coverage
run: npm run test:run -- --coverage
完了後、lcov レポートを Codecov や Coveralls にアップロードし、カバレッジの変化を視覚的に追跡できます。
実践的なアドバイス
- 100% を追求しない:カバレッジ数字が良く見えても、コード品質が高いとは限りません。重要なロジックをテストし、エッジケースはスキップしても構いません。
- まず主要パスをテスト:メインフローのテスト優先度が最も高く、例外ブランチは次。
- 定期的に不要なテストを削除:テストもメンテナンスが必要、古くなった冗長なものを削除。
- ウォッチモードを習慣に:開発時に
vitestをウォッチモードで実行し続け、問題があればすぐに発見。
まとめ
Vitest の核心的利点をまとめると:速い、簡単、使いやすい。
速い——コールドスタート 200ms、500 個のテストケースも約 8 秒で完了、Jest よりほぼ一桁速い。簡単——Vite と設定を共有、インストール後すぐ実行可能、transform や moduleNameMapper を設定する必要がない。使いやすい——API は Jest とほぼ同じ、移行コストが低い。
Vite を使用しているなら、迷わず Vitest を選択してください。Jest の ESM 設定問題に苦しむ理由はありません。
プロジェクトでまだ Jest を使用している場合、30 分かけて移行を試してみてください。まず Vitest をインストールし、テストファイルの import を変更するだけで、多くの場合すぐに動作します。
TDD について——複雑に考えすぎないでください。シンプルなユーティリティ関数から始め、先にテストを書いてからコードを書く。慣れると、「先に明確に考えてから着手する」方法が、逆に効率が高いことに気づくでしょう。
テストは負担ではなく、保障です。時間をかけて Vitest を適切に設定すれば、その後のコード記述がより安心になります。
Vitest ユニットテスト設定と TDD フロー
ゼロから Vitest を設定し、TDD 開発フローを実践する完全なステップ
⏱️ 目安時間: 30 分
- 1
ステップ1: Vitest をインストール
インストールコマンドを実行:
```bash
npm install -D vitest
```
システム要件:Vite >= 6.0.0、Node >= 20.0.0 - 2
ステップ2: vite.config.ts を設定
設定ファイルに test フィールドを追加:
```typescript
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
globals: true,
environment: 'node',
include: ['tests/**/*.test.ts'],
},
})
```
globals: true で describe、it、expect の毎回のインポートを省略可能。 - 3
ステップ3: 実行スクリプトを追加
package.json に追加:
```json
{
"scripts": {
"test": "vitest",
"test:run": "vitest run"
}
}
```
npm test はウォッチモード、npm run test:run は一回実行(CI 用)。 - 4
ステップ4: 最初のテストを書く
テストファイル tests/math.test.ts を作成:
```typescript
import { describe, it, expect } from 'vitest'
describe('Math', () => {
it('should add numbers', () => {
expect(1 + 1).toBe(2)
})
})
```
npm test を実行して設定が成功したか確認。 - 5
ステップ5: TDD フローを実践
テスト駆動開発フローに従う:
• Step 1:先にテストを書き、関数の期待される動作を定義
• Step 2:テストを通す最小限のコードを実装
• Step 3:境界テストを追加
• Step 4:リファクタリングして最適化
Vitest ウォッチモードで秒単位のフィードバックを獲得。 - 6
ステップ6: Coverage を設定
カバレッジ設定を追加:
```typescript
coverage: {
provider: 'v8',
reporter: ['text', 'html'],
thresholds: {
lines: 80,
functions: 80,
},
}
```
vitest run --coverage を実行してレポートを生成。
FAQ
Vitest と Jest の違いは何ですか?
Jest から Vitest に移行する方法は?
• Jest 関連パッケージをアンインストールし、vitest をインストール
• jest.config.js の設定を vite.config.ts に移行
• テストファイルの import { describe, it, expect } from 'jest' を from 'vitest' に変更
• jest.fn()、jest.mock() を vi.fn()、vi.mock() に変更
ほとんどの場合、30 分以内で移行が完了します。
Vitest はどのテスト環境をサポートしていますか?
Vitest で API リクエストをモックする方法は?
• vi.fn():単一の関数をモック、戻り値を事前設定
• vi.mock():モジュール全体をモック、すべてのエクスポートを置換
• vi.spyOn():実際の関数呼び出しを監視、実装は置換しない
各テスト後に vi.clearAllMocks() または vi.restoreAllMocks() で状態をクリアすることを忘れないでください。
Vitest の Coverage の設定方法は?
TDD 開発フローの核心は何ですか?
• レッド:先に失敗するテストを書く
• グリーン:テストを通す最小限のコードを書く
• リファクタ:コード構造を最適化
Vitest ウォッチモードと組み合わせると、コード変更後に秒単位でフィードバックが得られ、迅速なイテレーションが可能。先にテストを書くことで関数設計の思考が明確になります。
5 min read · 公開日: 2026年4月14日 · 更新日: 2026年4月14日
関連記事
Supabase Storage 実践ガイド:ファイルアップロード、CDN、アクセス制御
Supabase Storage 実践ガイド:ファイルアップロード、CDN、アクセス制御
Docker Compose 本番デプロイ:ヘルスチェック、再起動ポリシー、ログ管理
Docker Compose 本番デプロイ:ヘルスチェック、再起動ポリシー、ログ管理
Nginx パフォーマンスチューニング:gzip、キャッシュ、接続プール設定

コメント
GitHubアカウントでログインしてコメントできます