--- name: tdd-workflow description: 新機能の作成、バグ修正、コードのリファクタリング時にこのスキルを使用します。ユニット、統合、E2Eテストを含む80%以上のカバレッジでテスト駆動開発を強制します。 --- # テスト駆動開発ワークフロー このスキルは、すべてのコード開発が包括的なテストカバレッジを備えたTDDの原則に従うことを保証します。 ## 有効化するタイミング - 新機能や機能の作成 - バグや問題の修正 - 既存コードのリファクタリング - APIエンドポイントの追加 - 新しいコンポーネントの作成 ## コア原則 ### 1. コードの前にテスト 常にテストを最初に書き、次にテストに合格するコードを実装します。 ### 2. カバレッジ要件 - 最低80%のカバレッジ(ユニット + 統合 + E2E) - すべてのエッジケースをカバー - エラーシナリオのテスト - 境界条件の検証 ### 3. テストタイプ #### ユニットテスト - 個々の関数とユーティリティ - コンポーネントロジック - 純粋関数 - ヘルパーとユーティリティ #### 統合テスト - APIエンドポイント - データベース操作 - サービス間相互作用 - 外部API呼び出し #### E2Eテスト (Playwright) - クリティカルなユーザーフロー - 完全なワークフロー - ブラウザ自動化 - UI相互作用 ## TDDワークフローステップ ### ステップ1:ユーザージャーニーを書く ``` [役割]として、[行動]をしたい、それによって[利益]を得られるようにするため 例: ユーザーとして、セマンティックに市場を検索したい、 それによって正確なキーワードなしでも関連する市場を見つけられるようにするため。 ``` ### ステップ2:テストケースを生成 各ユーザージャーニーについて、包括的なテストケースを作成: ```typescript describe('Semantic Search', () => { it('returns relevant markets for query', async () => { // テスト実装 }) it('handles empty query gracefully', async () => { // エッジケースのテスト }) it('falls back to substring search when Redis unavailable', async () => { // フォールバック動作のテスト }) it('sorts results by similarity score', async () => { // ソートロジックのテスト }) }) ``` ### ステップ3:テストを実行(失敗するはず) ```bash npm test # テストは失敗するはず - まだ実装していない ``` ### ステップ4:コードを実装 テストに合格する最小限のコードを書く: ```typescript // テストにガイドされた実装 export async function searchMarkets(query: string) { // 実装はここ } ``` ### ステップ5:テストを再実行 ```bash npm test # テストは今度は成功するはず ``` ### ステップ6:リファクタリング テストをグリーンに保ちながらコード品質を向上: - 重複を削除 - 命名を改善 - パフォーマンスを最適化 - 可読性を向上 ### ステップ7:カバレッジを確認 ```bash npm run test:coverage # 80%以上のカバレッジを達成したことを確認 ``` ## テストパターン ### ユニットテストパターン (Jest/Vitest) ```typescript import { render, screen, fireEvent } from '@testing-library/react' import { Button } from './Button' describe('Button Component', () => { it('renders with correct text', () => { render() expect(screen.getByText('Click me')).toBeInTheDocument() }) it('calls onClick when clicked', () => { const handleClick = jest.fn() render() fireEvent.click(screen.getByRole('button')) expect(handleClick).toHaveBeenCalledTimes(1) }) it('is disabled when disabled prop is true', () => { render() expect(screen.getByRole('button')).toBeDisabled() }) }) ``` ### API統合テストパターン ```typescript import { NextRequest } from 'next/server' import { GET } from './route' describe('GET /api/markets', () => { it('returns markets successfully', async () => { const request = new NextRequest('http://localhost/api/markets') const response = await GET(request) const data = await response.json() expect(response.status).toBe(200) expect(data.success).toBe(true) expect(Array.isArray(data.data)).toBe(true) }) it('validates query parameters', async () => { const request = new NextRequest('http://localhost/api/markets?limit=invalid') const response = await GET(request) expect(response.status).toBe(400) }) it('handles database errors gracefully', async () => { // データベース障害をモック const request = new NextRequest('http://localhost/api/markets') // エラー処理のテスト }) }) ``` ### E2Eテストパターン (Playwright) ```typescript import { test, expect } from '@playwright/test' test('user can search and filter markets', async ({ page }) => { // 市場ページに移動 await page.goto('/') await page.click('a[href="/markets"]') // ページが読み込まれたことを確認 await expect(page.locator('h1')).toContainText('Markets') // 市場を検索 await page.fill('input[placeholder="Search markets"]', 'election') // デバウンスと結果を待つ await page.waitForTimeout(600) // 検索結果が表示されることを確認 const results = page.locator('[data-testid="market-card"]') await expect(results).toHaveCount(5, { timeout: 5000 }) // 結果に検索語が含まれることを確認 const firstResult = results.first() await expect(firstResult).toContainText('election', { ignoreCase: true }) // ステータスでフィルタリング await page.click('button:has-text("Active")') // フィルタリングされた結果を確認 await expect(results).toHaveCount(3) }) test('user can create a new market', async ({ page }) => { // 最初にログイン await page.goto('/creator-dashboard') // 市場作成フォームに入力 await page.fill('input[name="name"]', 'Test Market') await page.fill('textarea[name="description"]', 'Test description') await page.fill('input[name="endDate"]', '2025-12-31') // フォームを送信 await page.click('button[type="submit"]') // 成功メッセージを確認 await expect(page.locator('text=Market created successfully')).toBeVisible() // 市場ページへのリダイレクトを確認 await expect(page).toHaveURL(/\/markets\/test-market/) }) ``` ## テストファイル構成 ``` src/ ├── components/ │ ├── Button/ │ │ ├── Button.tsx │ │ ├── Button.test.tsx # ユニットテスト │ │ └── Button.stories.tsx # Storybook │ └── MarketCard/ │ ├── MarketCard.tsx │ └── MarketCard.test.tsx ├── app/ │ └── api/ │ └── markets/ │ ├── route.ts │ └── route.test.ts # 統合テスト └── e2e/ ├── markets.spec.ts # E2Eテスト ├── trading.spec.ts └── auth.spec.ts ``` ## 外部サービスのモック ### Supabaseモック ```typescript jest.mock('@/lib/supabase', () => ({ supabase: { from: jest.fn(() => ({ select: jest.fn(() => ({ eq: jest.fn(() => Promise.resolve({ data: [{ id: 1, name: 'Test Market' }], error: null })) })) })) } })) ``` ### Redisモック ```typescript jest.mock('@/lib/redis', () => ({ searchMarketsByVector: jest.fn(() => Promise.resolve([ { slug: 'test-market', similarity_score: 0.95 } ])), checkRedisHealth: jest.fn(() => Promise.resolve({ connected: true })) })) ``` ### OpenAIモック ```typescript jest.mock('@/lib/openai', () => ({ generateEmbedding: jest.fn(() => Promise.resolve( new Array(1536).fill(0.1) // 1536次元埋め込みをモック )) })) ``` ## テストカバレッジ検証 ### カバレッジレポートを実行 ```bash npm run test:coverage ``` ### カバレッジ閾値 ```json { "jest": { "coverageThresholds": { "global": { "branches": 80, "functions": 80, "lines": 80, "statements": 80 } } } } ``` ## 避けるべき一般的なテストの誤り ### FAIL: 誤り:実装の詳細をテスト ```typescript // 内部状態をテストしない expect(component.state.count).toBe(5) ``` ### PASS: 正解:ユーザーに見える動作をテスト ```typescript // ユーザーが見るものをテスト expect(screen.getByText('Count: 5')).toBeInTheDocument() ``` ### FAIL: 誤り:脆弱なセレクタ ```typescript // 簡単に壊れる await page.click('.css-class-xyz') ``` ### PASS: 正解:セマンティックセレクタ ```typescript // 変更に強い await page.click('button:has-text("Submit")') await page.click('[data-testid="submit-button"]') ``` ### FAIL: 誤り:テストの分離なし ```typescript // テストが互いに依存 test('creates user', () => { /* ... */ }) test('updates same user', () => { /* 前のテストに依存 */ }) ``` ### PASS: 正解:独立したテスト ```typescript // 各テストが独自のデータをセットアップ test('creates user', () => { const user = createTestUser() // テストロジック }) test('updates user', () => { const user = createTestUser() // 更新ロジック }) ``` ## 継続的テスト ### 開発中のウォッチモード ```bash npm test -- --watch # ファイル変更時に自動的にテストが実行される ``` ### プリコミットフック ```bash # すべてのコミット前に実行 npm test && npm run lint ``` ### CI/CD統合 ```yaml # GitHub Actions - name: Run Tests run: npm test -- --coverage - name: Upload Coverage uses: codecov/codecov-action@v3 ``` ## ベストプラクティス 1. **テストを最初に書く** - 常にTDD 2. **テストごとに1つのアサート** - 単一の動作に焦点 3. **説明的なテスト名** - テスト内容を説明 4. **Arrange-Act-Assert** - 明確なテスト構造 5. **外部依存関係をモック** - ユニットテストを分離 6. **エッジケースをテスト** - null、undefined、空、大きい値 7. **エラーパスをテスト** - ハッピーパスだけでなく 8. **テストを高速に保つ** - ユニットテスト各50ms未満 9. **テスト後にクリーンアップ** - 副作用なし 10. **カバレッジレポートをレビュー** - ギャップを特定 ## 成功指標 - 80%以上のコードカバレッジを達成 - すべてのテストが成功(グリーン) - スキップまたは無効化されたテストなし - 高速なテスト実行(ユニットテストは30秒未満) - E2Eテストがクリティカルなユーザーフローをカバー - テストが本番前にバグを検出 --- **覚えておいてください**:テストはオプションではありません。テストは自信を持ってリファクタリングし、迅速に開発し、本番の信頼性を可能にする安全網です。