mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-12 20:53:34 +08:00
Revert "feat(ecc): prune plugin 43→12 items, promote 7 rules to .claude/rules/ (#245)"
This reverts commit 1bd68ff534.
This commit is contained in:
409
docs/ja-JP/skills/tdd-workflow/SKILL.md
Normal file
409
docs/ja-JP/skills/tdd-workflow/SKILL.md
Normal file
@@ -0,0 +1,409 @@
|
||||
---
|
||||
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テストがクリティカルなユーザーフローをカバー
|
||||
- テストが本番前にバグを検出
|
||||
|
||||
---
|
||||
|
||||
**覚えておいてください**:テストはオプションではありません。テストは自信を持ってリファクタリングし、迅速に開発し、本番の信頼性を可能にする安全網です。
|
||||
Reference in New Issue
Block a user