AgentSkillsCN

tdd

测试驱动开发(TDD)技能。按照Red→Green→Refactor的循环流程编写代码。若有testing.md文件,则可与SDD协同使用;即使没有testing.md文件,也可独立应用。“/tdd 目标文件”“/tdd 功能说明”等用法皆可灵活运用。

SKILL.md
--- frontmatter
name: tdd
description: テスト駆動開発(TDD)スキル。Red→Green→Refactorサイクルでコードを実装。testing.mdがあればSDDと連携、なくてもスタンドアロンで使用可能。「/tdd 対象ファイル」「/tdd 機能説明」のように使用。
argument-hint: "[対象ファイルパス or 機能説明]"

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 単独実行の場合):

  1. 対象コードを分析

    • Serena MCPで get_symbols_overviewfind_symbol で構造把握
    • 関数のシグネチャ、入出力の型を確認
  2. テストケースを設計

    • 正常系(主要なユースケース)
    • 異常系(エラーハンドリング、null/undefined)
    • 境界値(0、空配列、最大値)
    • エッジケース(並行処理、タイミング依存)
  3. 設計をユーザーに提示

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(),
}));