Vitest 単体テスト実践:TDD ワークフローとカバレッジ設定
ターミナルに Test Suites: 1 failed, 47 passed と表示されている。1 行コードを直し、28 秒待ってテスト完了。また 1 行直すと、また 28 秒。
Jest から Vitest へ移行した人なら、この光景は馴染み深いでしょう。
当時、プロジェクトには約 500 件のテストがあり、npm test のたびに Hacker News を 2 ページ分読める時間がかかっていました。Vitest に切り替えてからは、同じテストが 3 秒強で終わるようになりました。
本記事では 2 つを扱います。Vitest でこの高速なテスト体験をどう実現するか、そして TDD(テスト駆動開発)でテストを書く負担をどう減らすか。価格フォーマット関数を題材に Red-Green-Refactor を一通り体験し、カバレッジ設定、Mock のコツ、Vitest UI というデバッグツールまで触れます。
なぜ Vitest + TDD なのか
まず数字から。
SitePoint の 2026 年比較では、5 万件のテストで Vitest は 3 秒、Jest は 28〜34 秒。少しの差ではなく、桁が違います。
速度だけが理由ではありません。Jest で ESM モジュールを扱った経験があれば、babel や transformer、jest.config.js の魔法文字列に祈る日々を思い出すでしょう。Vitest は ESM をネイティブサポートし、追加のトランスパイル設定は不要。書いたコードのままテストが走ります。
Vite ユーザーにはもう一つメリットがあります。Vitest は Vite 設定をそのまま使います。vite.config.ts のエイリアス、環境変数、プラグインがテスト環境にも引き継がれ、Jest の moduleNameMapper を二重管理する必要がありません。初めて気づいたとき、テスト設定がここまで楽でいいのかと少し呆れました。
ツールの話の次は手法。TDD は聞いたことはあっても続けている人は少ない。核心は Red-Green-Refactor:失敗するテスト(赤)→ 通る最小コード(緑)→ リファクタリング。直感に反するように感じるかもしれません。テストを先に?
利点は、書くコードの一行一行がテスト通過のため、ということ。余計なロジックや「念のため」の分岐が入りにくい。テスト先行で、関数の役割・戻り値・境界を先に考えざるを得ない。その制約が設計をはっきりさせます。
Vitest の watch モードはこのループと相性が良い。保存すればテストが即走り、結果がターミナルに出る。ウィンドウ切り替えも手動実行も不要。副操縦士が常に横で「ここ直したら落ちた」「今は全部緑」と教えてくれるイメージ。即時フィードバックが、自然とフロー状態へ導きます。
TDD 実践:関数をゼロから作る
話だけでは足りないので、TDD で formatPrice() を作ります。数値を通貨表示に変換する関数で、1234.5 を ¥1,234.50 にします。
Red フェーズ:失敗するテストを先に書く
プロジェクトで formatPrice.test.ts を新規作成:
// formatPrice.test.ts
import { describe, it, expect } from 'vitest'
import { formatPrice } from './formatPrice'
describe('formatPrice', () => {
it('数値を人民元の通貨形式にフォーマットする', () => {
expect(formatPrice(1234.5)).toBe('¥1,234.50')
})
})
この時点で npx vitest を実行すると、真っ赤なエラー:Cannot find module './formatPrice'。関数がまだないから当然。
これが Red。テストが失敗し、未実装の要件が定義された状態。先にコードを書くより、テストが本当に検証したいことを保証しやすい、というのが TDD の考え方です。
Green フェーズ:通る最小コードを書く
formatPrice.ts を作成し、テストを通すだけのコードを書きます:
// formatPrice.ts
export function formatPrice(value: number): string {
return '¥1,234.50' // まずは戻り値をハードコード
}
再実行すると緑。
「ズルじゃないの?」と思うかもしれません。TDD が求めるのは「ちょうど通る」コード。ハードコードでも最小ロジックでも、テストが通れば検証可能な土台ができます。次のテストを足し、コードを少しずつ育てていきます。
もう一つケースを追加:
it('異なる数値も正しく処理する', () => {
expect(formatPrice(0)).toBe('¥0.00')
expect(formatPrice(99.99)).toBe('¥99.99')
})
再び赤。ハードコードでは通らないので、本物のロジックが必要:
export function formatPrice(value: number): string {
return `¥${value.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',')}`
}
実行すれば再び全緑。正規表現は美しくないですが、今は動けば十分。
Refactor フェーズ:構造を最適化
テストが通ったので、安心してリファクタリングできます。壊せばすぐ赤に戻ります。
// リファクタリング後
export function formatPrice(value: number): string {
// Intl.NumberFormat の方が堅牢
return new Intl.NumberFormat('zh-CN', {
style: 'currency',
currency: 'CNY',
minimumFractionDigits: 2,
}).format(value)
}
テストは引き続き全緑。一サイクル完了。
Red-Green-Refactor の一巡です。失敗から始め、最小実装、そして改善。常にテストが守ってくれるので、怖くて手を止めなくていい。各ステップが小さい分、境界ケースを一度に全部考えなくて済み、テストが不足を教えてくれます。
実務では watch モードで回すことが多い。保存 → 自動実行 → 結果確認 → 修正 → 保存、数秒のループ。エディタから離れず、「直したらすぐ分かる」感覚は気持ちいいものです。
カバレッジ設定と CI 連携
テストを書いたら、どこまでカバーしたか知りたくなります。カバレッジレポートがその役目です。
基本設定
vitest.config.ts にカバレッジを追加:
// vitest.config.ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
coverage: {
provider: 'v8', // または 'istanbul'。v8 の方がデフォルトで速い
reporter: ['text', 'html', 'json-summary'],
reportsDirectory: './coverage',
include: ['src/**/*.ts'],
exclude: ['src/**/*.test.ts', 'src/types/**'],
thresholds: {
statements: 80,
branches: 75,
functions: 80,
lines: 80,
},
},
},
})
provider は v8 と istanbul。v8 は V8 エンジンのネイティブ API で速く、istanbul は古くからの選択肢で互換性重視。Vite / Node だけなら v8 で十分なことが多いです。
reporter は出力形式:text はターミナル、html は可視レポート、json-summary は CI 向け。
しきい値の考え方
thresholds は 4 軸あります:
- statements:文が実行された割合
- branches:if / else など分岐が両方テストされたか
- functions:関数が呼ばれた割合
- lines:行ベースのカバー率(statements と似るが計算方法が異なる)
目安は 75〜85%。低すぎると意味が薄く、高すぎるとチームが疲弊します。境界チェックやエラー処理は 100% にしにくい部分もあります。
GitHub Actions 連携
カバレッジの真価は、CI で基準未達の PR を止められること。.github/workflows/test.yml に例:
- name: Run tests with coverage
run: npm run test -- --coverage
- name: Check coverage threshold
run: |
COVERAGE=$(cat coverage/coverage-summary.json | jq '.total.lines.pct')
if (( $(echo "$COVERAGE < 80" | bc -l) )); then
echo "Coverage $COVERAGE% is below threshold 80%"
exit 1
fi
80% 未満なら PR はマージできません。提出前にテストを足す習慣がつきます。
レポートの読み方
npx vitest --coverage 後、ターミナルには例えば:
% Stmts % Branch % Funcs % Lines Uncovered Line
----------|----------|----------|----------|----------------
82.45 | 76.32 | 85.71 | 82.45 | 23-25, 67
Uncovered Line は未カバー行。coverage/index.html では緑がカバー済み、赤が未カバーです。
最初は 100% 狙いの強迫観念にもなりがちでしたが、80% 前後でコアロジックと主要分岐はだいたい押さえられます。残りは極端な境界で、無理にテストするとコスパが悪いことも多いです。
Mock 三種:vi.fn、vi.spy、vi.mock
テストで面倒なのは外部依存——API、タイマー、サードパーティライブラリ。Vitest は 3 つの Mock 手段を用意しています。
vi.fn():偽関数を作る
元の実装がどうでもよく、「どう呼ばれたか」だけ知りたいときは vi.fn()。
test('コールバックが 1 回呼ばれる', () => {
const callback = vi.fn()
callMeMaybe(callback)
expect(callback).toHaveBeenCalledTimes(1)
expect(callback).toHaveBeenCalledWith('hello')
})
callback は新規関数で、呼び出し回数・引数・戻り値を記録します。mockReturnValue や mockImplementation で挙動も指定できます。
vi.spy():本物の関数を監視
置き換えず、呼ばれたか・何が渡されたかを見たいときは vi.spyOn。
test('console.log が呼ばれる', () => {
const logSpy = vi.spyOn(console, 'log')
greet('World')
expect(logSpy).toHaveBeenCalledWith('Hello, World!')
logSpy.mockRestore() // 復元を忘れずに
})
spy は元の挙動を保ったまま監視します。終わったら mockRestore()。他テストへの影響を防ぐため必須です。
vi.mock():モジュール全体を置換
API レスポンスやライブラリごと差し替えるときは vi.mock。
// axios をモック
vi.mock('axios', () => ({
default: {
get: vi.fn(() => Promise.resolve({ data: { name: 'test' } }))
}
}))
test('getUser がユーザーデータを返す', async () => {
const user = await getUser(1)
expect(user.name).toBe('test')
expect(axios.get).toHaveBeenCalledWith('/users/1')
})
vi.mock はファイル先頭にホイストされるため、関数内や if の中に書いても意味がありません。モック内容は他変数に依存させられない点に注意。
どれを選ぶ?
- 偽関数だけ欲しい →
vi.fn() - 本物を覗きたい →
vi.spy() - モジュール丸ごと →
vi.mock()
覚え方:fn は「作る」、spy は「覗く」、mock は「丸ごと差し替え」。
クリーンアップ
テスト同士が干渉しないことが信頼性の前提。各テスト後に Mock を片付けます:
afterEach(() => {
vi.restoreAllMocks()
})
または設定でグローバルに:
test: {
restoreMocks: true
}
Vitest UI とデバッグのコツ
ターミナルでも足りますが、もっと直感的に見たいなら Vitest UI。
可視化 UI を起動
npx vitest --ui
ブラウザにテスト一覧と詳細が開きます。任意のテストをクリックすると出力、スタック、実行時間が見え、カバレッジボタンから HTML レポートにも飛べます。
失敗時のデバッグに向いています。ターミナルログを追わず、UI でエラーとコードを並べて、保存すれば自動更新。
watch モード:影響範囲だけ実行
日常開発では npx vitest の watch が便利。utils/formatPrice.ts を直せば関連テストだけ走り、全件再実行しません。
800 件超のプロジェクトでは、フル 4 秒に対し増分は数百 ms という差も珍しくありません。
デバッグの定番
単一テストだけ:it に .only
it.only('このテストだけ単独実行', () => {
// ...
})
スキップ:.skip
it.skip('一旦スキップ', () => {
// ...
})
スナップショット更新:コンポーネント構造を変えたあと
npx vitest -u # --update の短縮形
console.log:古くて確実。Vitest は console 出力を結果にそのまま表示します。
よくあるエラー
| エラー | 原因 | 対処 |
|---|---|---|
Cannot find module | パスエイリアス未設定 | vitest.config の alias を確認 |
vi.mock is not a function | import 方法の誤り | import { vi } from 'vitest' を使う |
| テストのタイムゾーン不一致 | デフォルト UTC | setup で process.env.TZ = 'Asia/Shanghai' |
特にタイムゾーンは CI でローカルだけ通る、という罠になりがち。半日かけて気づいた経験があります。
まとめ
要点は一つ。Vitest + TDD で、テストを書く苦痛をかなり減らせます。
速度面では Jest の数十秒が数秒になり、数字以上に体験が変わります。コード修正とテスト待ちの往復が減る。ESM ネイティブ、Vite 設定の再利用も、地味に効きます。
Red-Green-Refactor は最初は直感に反しても、数回回すと分かります。各ステップが小さく、都度検証でき、設計を一度に完璧にしなくていい。テストが不足と退行を教えてくれます。
カバレッジと Mock はツール層の話。習得すれば堅いテストが書けますが、本質は習慣——数字追いではなく、コードへの自信のため。
すでに Vite なら Jest からの移行コストは低い。npm add -D vitest と構文の微調整(ほぼ互換)で動きます。迷うなら小さなモジュールから watch の即時フィードバックを試してください。
今すぐ Vitest で最初のテストを走らせ、TDD のループを体感してみてください。「直したらすぐ分かる」安心感を知ると、テストが嫌いだった人でも考えが変わるかもしれません。
Vitest TDD 実践ワークフロー
TDD で関数をゼロから作り、カバレッジレポートを設定する
⏱️ 目安時間: 30 分
- 1
ステップ1: Vitest をインストール
Vite プロジェクトに Vitest を追加:
npm add -D vitest
追加設定は不要。Vitest が Vite 設定を自動で再利用します - 2
ステップ2: Red フェーズ:失敗するテストを書く
テストファイルを作成し、必ず失敗するテストを書きます:
import { describe, it, expect } from 'vitest'
import { formatPrice } from './formatPrice'
describe('formatPrice', () => {
it('通貨をフォーマットする', () => {
expect(formatPrice(1234.5)).toBe('¥1,234.50')
})
})
npx vitest を実行し、テストが失敗(赤)することを確認 - 3
ステップ3: Green フェーズ:最小コードを書く
実装ファイルを作成し、テストを通す最小限のコードを書きます:
export function formatPrice(value: number): string {
return '¥1,234.50' // まずはハードコード
}
テストを実行し、成功(緑)を確認 - 4
ステップ4: Refactor フェーズ:コードを最適化
Intl.NumberFormat でリファクタリング:
export function formatPrice(value: number): string {
return new Intl.NumberFormat('zh-CN', {
style: 'currency',
currency: 'CNY',
}).format(value)
}
テストが引き続き通ることを確認 - 5
ステップ5: カバレッジを設定
vitest.config.ts に追加:
test: {
coverage: {
provider: 'v8',
thresholds: { statements: 80, branches: 75 }
}
}
npx vitest --coverage でレポートを確認
FAQ
Vitest と Jest の主な違いは?
TDD の Red-Green-Refactor サイクルはどう回す?
• Red:失敗するテストを先に書き、要件を定義
• Green:テストを通す最小コードを書く(ハードコードでも可)
• Refactor:テストの保護下で構造を最適化
各ステップは小さく、認知負荷が低く、テストが常に安全網になります。
カバレッジしきい値はどのくらいが妥当?
vi.fn / vi.spy / vi.mock はどう使い分ける?
• vi.fn():新しい偽関数を作り、呼び出し回数と引数を記録
• vi.spy():既存関数を監視し、元の挙動を保持。終了後は mockRestore() が必要
• vi.mock():モジュール全体を置換。API やサードパーティライブラリのモック向け
覚え方:fn は「作る」、spy は「覗く」、mock は「丸ごと差し替え」。
Vitest の watch モードの利点は?
テストのタイムゾーン問題はどう解決する?
6分で読めます · 公開日: 2026年4月29日 · 更新日: 2026年6月15日
Vitest テストガイド
検索からこのページに来た場合は、前後の記事もあわせて読むと同じテーマの理解がかなり早く深まります。
前の記事
Vitest ユニットテスト実践:設定から TDD 開発フローまで
Vitest ユニットテストをゼロからマスター:インストール・設定、TDD 開発フロー、Mocking 高度テクニック、Coverage 設定。Vitest は Jest より 10 倍速く、Jest API と互換性があり、30 分で移行できます。
第 1 / 3 記事
次の記事
Vitest コンポーネントテスト実践:Browser Mode と Playwright 連携
Vitest Browser Mode 実践ガイド。Playwright 連携の設定、React/Vue コンポーネントテスト、CI のカバレッジゲート、jsdom と実ブラウザテストの違いを解説します。
第 3 / 3 記事
関連記事
セルフホスト Dev Sandbox:Docker と Go でプレビュー環境を作る
セルフホスト Dev Sandbox:Docker と Go でプレビュー環境を作る
Cloudflare Pro か Business か?3 つの軸で判断するアップグレード判断ツリー
Cloudflare Pro か Business か?3 つの軸で判断するアップグレード判断ツリー
Docker ミラーソース速度テスト実践:3 つの方法 + 自動切り替えスクリプト
コメント
GitHubアカウントでログインしてコメントできます