TDD(テスト駆動開発)スキル
ワークフロー概要
code
テスト設計 → Red(失敗テスト) → Green(最小実装) → Refactor(改善) → 完了
🔴 コミット 🟢 コミット 🔵 コミット
原則: テストを先に書く。テストなしの実装コミットは行わない。
適用判定
TDD必須(このスキルで実装)
| 対象 | 理由 |
|---|---|
lib/ ロジック関数 | 純粋関数、入出力が明確 |
hooks/ カスタムフック | renderHookで検証可能 |
components/ のロジック部分 | 状態遷移・イベント処理 |
| バグ修正 | 回帰テストで再発防止 |
TDD対象外(別アプローチ)
| 対象 | 代替手法 |
|---|---|
| ビジュアル調整(配色・レイアウト) | ビジュアル反復型フロー |
docs/ ドキュメント | そのまま編集 |
chore 設定変更 | そのまま編集 |
Phase 1: テスト設計
1.1 Working Documents連携(SDDモード)
testing.md が存在する場合(/fix-issue から呼ばれた場合):
code
docs/working/{YYYYMMDD}_{Issue番号}_{タイトル}/testing.md
→ テストケース・カバレッジ目標を読み込み
→ これをテスト設計のインプットとする
1.2 スタンドアロンモード
testing.md がない場合(/tdd 単独実行の場合):
- •
対象コードを分析
- •Serena MCPで
get_symbols_overview→find_symbolで構造把握 - •関数のシグネチャ、入出力の型を確認
- •Serena MCPで
- •
テストケースを設計
- •正常系(主要なユースケース)
- •異常系(エラーハンドリング、null/undefined)
- •境界値(0、空配列、最大値)
- •エッジケース(並行処理、タイミング依存)
- •
設計をユーザーに提示
code
📋 テスト設計: 対象: lib/coffee-quiz/gamification.ts - calculateXP() テストケース: 1. ✅ 正常系: easy → 10XP, medium → 20XP, hard → 30XP 2. ✅ 正常系: ストリークボーナス適用 3. ❌ 異常系: 不正な難易度 → エラー 4. 🔲 境界値: ストリーク0日 このテスト設計で進めますか?
⚠️ スタンドアロンモードではユーザー確認を取る。SDDモードでは testing.md を信頼して進行。
Phase 2: Red(失敗テスト作成)🔴
2.1 テストファイル作成
テスト対象のファイルと同じディレクトリ、または隣接する __tests__/ に作成:
code
対象: lib/coffee-quiz/gamification.ts テスト: lib/coffee-quiz/gamification.test.ts
2.2 テストコード作成ルール
typescript
import { describe, it, expect } from 'vitest';
describe('calculateXP', () => {
// Phase 1のテストケースをそのまま実装
it('easyの場合10XPを返す', () => {
expect(calculateXP('easy')).toBe(10);
});
it('不正な難易度の場合エラーを投げる', () => {
expect(() => calculateXP('invalid' as any)).toThrow();
});
});
テストの品質基準:
- •テスト名は日本語で「何をしたら何が起きる」を記述
- •1テスト1アサーション を基本とする(関連アサーションの複数は許容)
- •AAAパターン(Arrange-Act-Assert)に従う
- •モックは最小限(vi.mock の hoisting 問題に注意)
2.3 Red確認
bash
npm run test -- 対象テストファイル
全テストが失敗する(Red)ことを確認。
既存テストが壊れていないかも確認:
bash
npm run test
2.4 Redコミット
bash
git add 対象テストファイル git commit -m "test(#Issue番号): 失敗テストを追加 - 機能説明"
⚠️ Issue番号がない場合(スタンドアロン)はスコープにファイル名を使用:
bash
git commit -m "test(gamification): 失敗テストを追加 - calculateXP"
Phase 3: Green(最小実装)🟢
3.1 実装の原則
テストを通す最小限のコードを書く。
- •過度な抽象化をしない
- •将来の拡張を考慮しない
- •テストが要求する振る舞いだけを実装
3.2 反復サイクル
code
実装を少し書く
↓
テスト実行: npm run test -- 対象テストファイル
↓
合格? ─── No → 実装を修正(ループ)
│
Yes
↓
全テスト合格? ─── No → 次のテストケースへ(ループ)
│
Yes
↓
Green達成 ✅
3.3 Green確認
bash
# 対象テストが全て合格 npm run test -- 対象テストファイル # 既存テストも壊れていない npm run test
3.4 Greenコミット
bash
git add 対象ファイル(実装 + テスト更新があれば) git commit -m "feat(#Issue番号): 機能説明の実装"
Phase 4: Refactor(改善)🔵
4.1 リファクタリング対象の判断
以下のいずれかに該当する場合のみ実施(該当しなければスキップ):
- •重複コードがある
- •関数が長すぎる(20行以上の目安)
- •命名が不適切
- •GUIDELINES.mdのコーディング規約に違反
4.2 リファクタリングの原則
テストを変更しない。テストが合格し続けることを保証。
code
コード改善
↓
テスト実行: npm run test -- 対象テストファイル
↓
合格? ─── No → 改善を修正(リファクタリングがバグを入れた)
│
Yes
↓
完了 ✅
4.3 Refactorコミット(変更があった場合のみ)
bash
git add 対象ファイル git commit -m "refactor(#Issue番号): 機能説明のリファクタリング"
Phase 5: 完了
5.1 最終検証
bash
npm run test npm run lint
5.2 カバレッジ確認
bash
npm run test -- --coverage 対象ディレクトリ
カバレッジ目標:
- •
lib/: 90%以上 - •
hooks/: 85%以上 - •
components/: 75%以上
5.3 testing.md更新(SDDモード時のみ)
testing.mdが存在する場合、完了状態に更新:
markdown
## テスト実施結果 - ✅ 全テストケース合格 - カバレッジ: XX% - 実施日: YYYY-MM-DD
5.4 完了報告
code
✅ TDDサイクル完了 📊 結果: - テストケース: X件(全合格) - カバレッジ: XX% - コミット: 🔴 Red → 🟢 Green → 🔵 Refactor 📁 作成/変更ファイル: - lib/coffee-quiz/gamification.test.ts(新規) - lib/coffee-quiz/gamification.ts(修正)
リファレンス
vi.mock の hoisting 問題
typescript
// ❌ NG: ファクトリ関数内で外部変数を参照
const MOCK_DATA = { value: 123 };
vi.mock('@/module', () => ({ data: MOCK_DATA }));
// ✅ OK: ファクトリ関数内で直接定義
vi.mock('@/module', () => ({ data: { value: 123 } }));
非同期フックのテストパターン
typescript
const { result } = renderHook(() => useMyHook());
// isHydrated等の初期化を待つ
await act(async () => {
await vi.runAllTimersAsync();
});
// テスト実行
await act(async () => {
await result.current.someFunction();
});
デバウンス処理のテスト
typescript
vi.useFakeTimers();
await act(async () => {
await debouncedFunction();
});
await act(async () => {
vi.advanceTimersByTime(1000);
await vi.runAllTimersAsync();
});
モックパスの完全一致
typescript
// 実際のimportパスと完全一致させる
vi.mock('@/lib/coffee-quiz/fsrs', () => ({
fsrs: vi.fn(),
}));