mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-05 00:33:27 +08:00
410 lines
11 KiB
Markdown
410 lines
11 KiB
Markdown
---
|
||
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(<Button>Click me</Button>)
|
||
expect(screen.getByText('Click me')).toBeInTheDocument()
|
||
})
|
||
|
||
it('calls onClick when clicked', () => {
|
||
const handleClick = jest.fn()
|
||
render(<Button onClick={handleClick}>Click</Button>)
|
||
|
||
fireEvent.click(screen.getByRole('button'))
|
||
|
||
expect(handleClick).toHaveBeenCalledTimes(1)
|
||
})
|
||
|
||
it('is disabled when disabled prop is true', () => {
|
||
render(<Button disabled>Click</Button>)
|
||
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
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
## 避けるべき一般的なテストの誤り
|
||
|
||
### ❌ 誤り:実装の詳細をテスト
|
||
```typescript
|
||
// 内部状態をテストしない
|
||
expect(component.state.count).toBe(5)
|
||
```
|
||
|
||
### ✅ 正解:ユーザーに見える動作をテスト
|
||
```typescript
|
||
// ユーザーが見るものをテスト
|
||
expect(screen.getByText('Count: 5')).toBeInTheDocument()
|
||
```
|
||
|
||
### ❌ 誤り:脆弱なセレクタ
|
||
```typescript
|
||
// 簡単に壊れる
|
||
await page.click('.css-class-xyz')
|
||
```
|
||
|
||
### ✅ 正解:セマンティックセレクタ
|
||
```typescript
|
||
// 変更に強い
|
||
await page.click('button:has-text("Submit")')
|
||
await page.click('[data-testid="submit-button"]')
|
||
```
|
||
|
||
### ❌ 誤り:テストの分離なし
|
||
```typescript
|
||
// テストが互いに依存
|
||
test('creates user', () => { /* ... */ })
|
||
test('updates same user', () => { /* 前のテストに依存 */ })
|
||
```
|
||
|
||
### ✅ 正解:独立したテスト
|
||
```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テストがクリティカルなユーザーフローをカバー
|
||
- テストが本番前にバグを検出
|
||
|
||
---
|
||
|
||
**覚えておいてください**:テストはオプションではありません。テストは自信を持ってリファクタリングし、迅速に開発し、本番の信頼性を可能にする安全網です。
|