Vitest 単体テスト実践:TDD ワークフローとカバレッジレポート
午前3時。ターミナルの 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年に比較テストを行いました:50,000テストケース、Vitest は3秒で完了。Jest?28〜34秒。これは小さな違いではなく、桁違いです。
速度は一つの理由です。Jest で ESM モジュールを使ったことがあれば、その壁にぶつかったでしょう—babel をインストール、transformers を設定、jest.config.js の魔法の文字列が実際に動くことを祈る。Vitest は違います。ESM をネイティブサポート、トランスパイル設定不要。コードは書いた通りに実行されます、シンプルで明確。
もう一つ Vite ユーザーに嬉しい点:Vitest は Vite 設定を再利用します。vite.config.ts で設定した alias、環境変数、プラグイン—テスト環境が自動的に継承します。Jest 用に別の moduleNameMapper を書く必要はありません。これを最初に発見した時、数秒呆然と座っていました—テスト設定が本当にこんなに簡単だったんだ?
では方法について話しましょう。TDD—多くの人が聞いたことがありますが、続ける人は少ない。核心は Red-Green-Refactor というサイクル:最初に失敗するテストを書く(赤)、次にちょうど通るコードを書く(緑)、最後にリファクタリングして整理する。逆直感的に聞こえますよね?コードより先にテストを書く?
でもここにメリットがあります:書くすべてのコード行はテストを通すために存在します。余分なロジックなし、「念のため」のコードなし。テストが先にあるため、関数が何をすべきか、何を返すか、境界がどこかを先に考えざるを得ません。この制約が実際に設計をより明確にします。
Vitest の watch モードがこのサイクルを非常にスムーズにします。ファイルを保存、テストが即座に実行、結果がターミナルに直接表示。ウィンドウ切り替え不要、手動コマンド不要—常にチェックするコパイilotみたい:「ねえ、その変更でテスト壊れた」「全部緑になった」。その即時フィードバックが、気づかないうちにフロー状態に入れます。
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 フェーズ—失敗テストは、まだ実装されていない要件を定義したことを意味します。先にテストを書くのは奇妙だと思う人もいますが、考えてみてください:先にコードを書いて後でテストを書くなら、テストが本当に意図したものをチェックしているかどうか分かりませんよね?
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 は2つの選択:v8 と istanbul。v8 は V8 エンジンのネイティブカバレッジ API を使い、より速い;istanbul は古いソリューションで、互換性が良い。純粋な Vite/Node 環境なら、v8 で十分。
reporter は出力フォーマットを指定:text はターミナルに印刷、html は視覚レポート生成、json-summary は CI ツール用。
しきい値設定
thresholds を説明します。4つの次元があります:
- statements:文カバレッジ、どれくらいのコード行が実行されたか
- branches:分岐カバレッジ、各 if/else 分岐がテストされたか
- functions:関数カバレッジ、どれくらいの関数が呼ばれたか
- lines:行カバレッジ、statements と似ているが計算方法が少し違う
通常 thresholds を75%-85%に設定します。低すぎると意味ない、高すぎるとチームを疲弊させる—一部のコード(境界チェック、エラー処理)は本当に100%到達できない。
GitHub Actions 統合
カバレッジの本当の価値:しきい値に達しない 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 を開くと詳細な視覚レポートが見れます—緑はテスト済み、赤は未テスト。
正直、最初カバレッジ追跡始めた時、各行をテストすることに OCD がありました。後に必要ないと気づきました。80%カバレッジは通常コアロジックと主分岐をカバーします。残り20%は極端エッジケース—そこを強制テストは時間浪費です。
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 文内に書いても関係ない。だから mock 内容は他の変数に依存できない。
どれを選ぶ?
簡単な答え:
- 偽関数が必要?
vi.fn() - 実関数を監視したい?
vi.spy() - モジュール全体を置換?
vi.mock()
以前この3つを混同していました。後に覚え方を発見:fn は「偽を作る」,spy は「盗み見る」,mock は「全部換える」。ちょっとキャッチー。
クリーンアップ重要
テスト間で相互影響しない—それが信頼性の基盤。各テスト後、mock をクリーンアップ:
afterEach(() => {
vi.restoreAllMocks()
})
または vitest 設定でグローバルに有効化:
test: {
restoreMocks: true
}
Vitest UI とデバッグ Tips
ターミナルテスト結果は十分ですが、より視覚体験が欲しいなら、Vitest UI を試して。
視覚インターフェース起動
npx vitest --ui
ブラウザが自動的に開き、左側テストリスト、右側詳細。任意テストをクリックして、完全な出力、エラースタック、実行時間を見れます。カバレッジボタンもあります—前述の HTML レポートを開きます。
これはデバッグに最適。テスト失敗?ターミナルログを探す必要ない—UI で直接エラー情報を見る、横にコードがある。修正、保存、インターフェース自動リフレッシュ。
watch モード:影響テストだけ実行
通常開発時、npx vitest で watch モードを走ります。増分テストはスマート—utils/formatPrice.ts を変更すると、そのファイルに関連するテストだけ実行、全部は再実行しない。
テストが多くなると、この差分実行は大幅な時間節約。800+テストのプロジェクト:全実行は4秒、増分は通常数百ミリ秒。
デバッグ Tips
テスト失敗?私が使う方法:
単一テスト実行:it の後に .only を追加
it.only('このテストに問題、単独実行', () => {
// ...
})
テストスキップ:.skip を追加
it.skip('今はスキップ', () => {
// ...
})
スナップショット更新:コンポーネント構造変更、スナップショットテスト失敗
npx vitest -u # -u は --update の略
console.log デバッグ:はい、古い方法が最良。Vitest は console 出力をテスト結果に完全に表示します。
共通エラー
| エラー | 原因 | 解決 |
|---|---|---|
Cannot find module | パス alias が未設定 | vitest.config の alias をチェック |
vi.mock is not a function | インポート方法エラー | import { vi } from 'vitest' を使う |
| テストタイムゾーン違う | デフォルトは UTC | setup で process.env.TZ = 'Asia/Tokyo' を設定 |
これら全部踏んだ罠。特にタイムゾーン—CI でローカル時間テスト全部失敗、半日デバッグしてタイムゾーン問題と気づきました。
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 実行してレポートを見る
結論
これ全部話した後、結局一つのこと:Vitest + TDD がテスト書くことをあまり苦痛にしない。
速度、Jest の数十秒から数秒—この変化は数字だけじゃなく、体験です。コード変更とテスト待ちの間で切り替える必要ない、「1行変更、永久待つ」拷問ない。ネイティブ ESM サポート、Vite 設定再利用—本当の便利さ。
TDD の Red-Green-Refactor サイクル、逆直感的に聞こえるけど、数回試すとメリットが分かります:各ステップは小さい、各ステップ検証、精神的負荷は軽い。最初に完璧ソリューション設計する必要ない—テストが問題発見、繰り返し改善。
カバレッジ設定と Mock テクニックはツールレベル—マスターするとより堅牢テストを書ける。本当の目標はテスト書く習慣を養う—数字追跡のためじゃなく、コードに自信を持つため。
プロジェクトが既に Vite を使っているなら、Jest から Vitest 移行はほぼコストゼロ。npm add -D vitest を追加、Jest テスト構文を Vitest に変更(ほぼ同じ)、走る。まだ迷っている?小さなモジュールで試して、watch モードの即時フィードバックを体験して。
今 Vitest で最初のテストを走らせて、TDD サイクルを試して。「何か変更して即座に正しいか分かる」自信を感じて。私みたいに、テスト書くことを好きになるかもしれない。
FAQ
Vitest と Jest の主な違いは?
TDD の Red-Green-Refactor サイクルはどうやる?
• Red:最初に失敗テストを書く、要件を定義
• Green:テストを通す最小コードを書く—ハードコード OK
• Refactor:テスト保護下でコード構造改善
各ステップは小さい、精神的負荷軽い、テスト全程保護。
カバレッジしきい値はどれくらい設定すべき?
vi.fn、vi.spy、vi.mock どう選ぶ?
• vi.fn():新偽関数作成、呼び出しと引数追跡
• vi.spy():実関数監視、元動作保持、使用後 mockRestore() 必要
• vi.mock():モジュール全体置換、API やサードパーティ lib シミュレート用
覚え方:fn は偽作る、spy は盗み見る、mock は全部換える。
Vitest watch モードの利点は?
テストタイムゾーン問題どう解決?
6 min read · 公開日: 2026年4月29日 · 更新日: 2026年4月29日
関連記事
GitHub Actions Matrix ビルド:マルチプラットフォーム・マルチバージョン並列テストの実践ガイド
GitHub Actions Matrix ビルド:マルチプラットフォーム・マルチバージョン並列テストの実践ガイド
Nginx ロードバランシング実践:upstream設定とヘルスチェック
Nginx ロードバランシング実践:upstream設定とヘルスチェック
Supabase Realtime 実践:WebSocket 接続管理と再接続戦略


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