Vitest ユニットテスト実践:設定から TDD 開発フローまで
Jest で ESM プロジェクトを設定するのに、どれだけ手間がかかるでしょうか。ts-jest、babel-jest、jest.config.js……さらに、さまざまなモジュール解決の問題にも対応する必要があります。私はかつて、Jest に .vue ファイルの import 文を正しく認識させるためだけに、午後を丸ごと費やしたことがあります。
ところが Vitest なら、たった 1 行の設定で済みます。
これは大げさな話ではありません。ある 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 以上が必要です。比較的新しいプロジェクトなら、たいてい満たしているはずです。
インストール
コマンドは 1 つだけです:
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('しばらく実行しない', () => { ... })
この 2 つは 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。
2 つのテストがどちらも緑になりましたか?よし、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():単一の関数を Mock
最もシンプルな使い方で、ダミーの関数を作成します:
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():モジュール全体を Mock
ある関数をテストしたいけれど、それが外部 API に依存している場合は、そのモジュールごと Mock します:
import { vi, describe, it, expect, beforeEach } from 'vitest'
import { fetchUser } from './api'
import { UserService } from './UserService'
// api モジュールを Mock
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() はモジュールの import より前に実行されるので、ファイルの先頭に置きます。
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 よりもおだやかです。関数は通常どおり動作し、「監視役」が 1 つ増えるだけです。
グローバルオブジェクトを Mock
テスト時に本当に HTTP リクエストを送りたくない場合は、グローバルの fetch を Mock します:
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve({ data: 'mocked' })
}))
あるいは Vitest の vi.stubGlobal を使って、window、localStorage などのブラウザのグローバルオブジェクトを Mock できます。
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 に 1 ステップ追加します:
# .github/workflows/test.yml
- name: Run tests with coverage
run: npm run test:run -- --coverage
実行後は lcov レポートを Codecov や Coveralls にアップロードして、カバレッジの変化を可視化して追跡できます。
実用的なアドバイス
- 100% を追い求めない:カバレッジの数字が立派でも、コード品質が高いとは限りません。重要なロジックをテストし、エッジケースは見逃してもかまいません。
- まずコアパスをテスト:主要なフローのテストが最優先で、例外分岐はその次です。
- 不要なテストを定期的に整理:テストもメンテナンスが必要です。古くなったもの、冗長なものは削除しましょう。
- Watch モードを習慣に:開発中は
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 を import する必要がなくなります。 - 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 リクエストをどう Mock しますか?
• vi.fn():単一の関数を Mock し、戻り値を事前設定
• vi.mock():モジュール全体を Mock し、すべてのエクスポートを置き換え
• vi.spyOn():実際の関数呼び出しを監視し、実装は置き換えない
各テストの後に vi.clearAllMocks() か vi.restoreAllMocks() で状態をクリーンにすることを忘れないでください。
Vitest の Coverage はどう設定しますか?
TDD 開発フローの核心は何ですか?
• レッド:先に失敗するテストを書く
• グリーン:テストを通す最小限のコードを書く
• リファクタリング:コード構造を最適化
Vitest の監視モードと組み合わせれば、コードを変更するたびに秒単位でフィードバックが返り、素早くイテレーションできます。先にテストを書くことで、関数の設計方針も整理しやすくなります。
4分で読めます · 公開日: 2026年4月14日 · 更新日: 2026年6月8日
関連記事
セルフホスト Dev Sandbox:Docker と Go でプレビュー環境を作る
セルフホスト Dev Sandbox:Docker と Go でプレビュー環境を作る
Cloudflare Pro か Business か?3 つの軸で判断するアップグレード判断ツリー
Cloudflare Pro か Business か?3 つの軸で判断するアップグレード判断ツリー
社内ネットワーク Docker pull タイムアウトのトラブルシューティング:DNS・プロキシ・ミラー加速の完全ガイド
コメント
GitHubアカウントでログインしてコメントできます