📝
claude code、テスト駆動開発してもエラー起きる問題
TDD実践で陥りがちな罠と根本的なプロセス改善策
はじめに
テスト駆動開発(TDD)を実践していても、実際のUIで動かすとエラーが頻発する...そんな経験はありませんか?
「テストは通っているのに、なぜ本番で動かないのか?」
この記事では、実際のプロジェクトで遭遇したTDDの落とし穴と、それを根本的に解決するプロセス改善策を紹介します。
🚨 よくある問題:テストは通るのに実際は動かない
実例:家事管理アプリの開発で起きた問題
Next.js + TypeScript + Prismaで家事管理アプリを開発していた時のことです。
// ✅ このテストは通る
it('should create chore successfully', async () => {
const response = await POST(request)
expect(response.status).toBe(201)
})
// ✅ このテストも通る
it('should display categories in form', () => {
render(<ChoreForm categories={mockCategories} />)
expect(screen.getByText('掃除')).toBeInTheDocument()
})
しかし、実際にブラウザで操作すると...
❌ カテゴリが表示されない
❌ "Household not found" エラー
❌ ページリロード後にデータが消える
なぜテストが通っているのに動かないのか?
🔍 根本原因の分析
1. テスト環境とランタイム環境の乖離
# テスト実行時
✅ テスト用データベース(独立環境)
✅ モックデータを使用
✅ 全て正常動作
# 実際のUI確認時
❌ 開発用データベース(テストで消去済み)
❌ 実際のAPIを呼び出し
❌ データが存在しないためエラー
2. 統合テストの不足
// ❌ あったのは単体テスト
describe('API', () => {
it('should return data', () => { ... })
})
describe('UI', () => {
it('should render', () => { ... })
})
// ✅ 不足していたのは統合テスト
describe('API ↔ UI Integration', () => {
it('should display API data in UI', () => { ... })
})
3. データセットアップの依存関係
// UI側のコード
const householdId = "test-household-id" // ←これが存在しない!
テストでは自動生成されるIDを使い、UIでは固定IDを使用。両者が一致していませんでした。
🛠️ 解決策:真のTDDプロセス構築
A. 環境分離戦略
1. データベースを環境別に完全分離
// lib/config.ts
export const config = {
database: {
url: process.env.NODE_ENV === 'test'
? 'file:./test.db'
: 'file:./dev.db'
},
testData: {
householdId: process.env.NODE_ENV === 'test'
? 'test-household-' + Date.now()
: 'test-household-id'
}
}
2. 環境セットアップスクリプト
{
"scripts": {
"test:setup": "NODE_ENV=test npm run db:reset && npm run db:seed",
"test:tdd": "npm run test:setup && npm run test:watch",
"dev:setup": "NODE_ENV=development npm run setup:dev",
"dev:reset": "npm run dev:setup && npm run dev"
}
}
B. 完全統合テスト戦略
3. レイヤー間統合テスト
// tests/integration/api-ui.test.ts
describe('カテゴリ表示 統合テスト', () => {
beforeEach(async () => {
// 実際のデータベースにテストデータ作成
await createTestData()
})
it('should display categories from API in UI', async () => {
// 1. 実際のAPIを呼び出し
const response = await fetch('/api/categories')
const categories = await response.json()
// 2. UIコンポーネントに実データを渡して表示確認
render(<ChoreForm categories={categories} />)
// 3. APIのデータがUIに正しく表示されることを確認
expect(screen.getByText('掃除')).toBeInTheDocument()
expect(screen.getByText('料理')).toBeInTheDocument()
})
})
4. E2Eワークフローテスト
// tests/e2e/chore-creation.test.ts
describe('家事作成 E2E', () => {
it('should complete entire user workflow', async () => {
// 1. ページアクセス
await page.goto('/chores')
// 2. フォーム表示
await page.click('button:has-text("新しい家事を追加")')
// 3. カテゴリ選択(実際のデータベースから取得)
await page.selectOption('select[name="category"]', '掃除')
// 4. 家事作成
await page.fill('input[name="title"]', 'リビング掃除')
await page.click('button:has-text("作成")')
// 5. 一覧表示確認
await expect(page.locator('text=リビング掃除')).toBeVisible()
// 6. ページリロード後の永続化確認
await page.reload()
await expect(page.locator('text=リビング掃除')).toBeVisible()
})
})
C. 改善されたTDDプロセス
新しい実装フロー
## Phase 1: 環境準備
- [ ] テスト環境のデータベースリセット
- [ ] 開発環境のデータベースリセット
- [ ] 両環境でのシードデータ確認
## Phase 2: テスト作成(重要:下から上へ)
- [ ] E2Eテスト作成(ユーザーワークフロー全体)
- [ ] 統合テスト作成(API ↔ UI)
- [ ] 単体テスト作成(個別機能)
- [ ] 全テスト実行 → RED確認
## Phase 3: 実装
- [ ] 最小実装
- [ ] 単体テスト → GREEN
- [ ] 統合テスト → GREEN
- [ ] E2Eテスト → GREEN
## Phase 4: 現実確認
- [ ] 開発環境でのデータセットアップ
- [ ] ブラウザでの手動確認
- [ ] 全自動テスト再実行
## Phase 5: 保護
- [ ] Git commit & push
- [ ] ドキュメント更新
D. 自動化による人的エラーの排除
5. プリコミットフック
{
"husky": {
"hooks": {
"pre-commit": "npm run check:all && npm run test:integration"
}
}
}
6. CI/CDパイプライン
# .github/workflows/test.yml
name: Complete Test Suite
on: [push, pull_request]
jobs:
test:
steps:
- name: Setup test environment
run: npm run test:setup
- name: Run unit tests
run: npm run test:unit
- name: Run integration tests
run: npm run test:integration
- name: Run E2E tests
run: npm run test:e2e
- name: Verify development environment
run: npm run dev:setup && npm run check:manual
🎯 重要な気づき:TDDの本質
TDDの誤解と真実
❌ 誤解: テストがあれば大丈夫
✅ 真実: テストが現実を正確に反映していれば大丈夫
❌ 誤解: 単体テストだけで十分
✅ 真実: 単体 + 統合 + E2E の組み合わせが必要
❌ 誤解: モックを使えば速い
✅ 真実: 実データでの検証も必須
テストピラミッドの再解釈
/\
/E2E\ ← 少数だが重要(ユーザー視点の検証)
/______\
/Integration\ ← 中程度(システム間連携の検証)
/__________\
/ Unit Tests \ ← 多数(個別機能の検証)
📋 実装チェックリスト
即座に実装すべき改善(優先度:高)
-
データベース環境分離
cp prisma/dev.db prisma/test.db
-
統合テストファイル作成
// tests/integration/ui-workflow.test.ts
-
開発環境チェックスクリプト
npm run dev:verify
段階的に導入(優先度:中)
- E2Eテストフレームワーク導入(Playwright推奨)
- プリコミットフック設定
- CI/CD改善
長期的改善(優先度:低)
- ビジュアルリグレッションテスト
- パフォーマンステスト
- セキュリティテスト
まとめ
TDDで「テストは通るのに動かない」問題は、テスト環境と現実の乖離が根本原因です。
解決の鍵は:
- 環境の一致: テスト ≈ 開発 ≈ 本番
- 完全なカバレッジ: 単体 + 統合 + E2E
- 自動化: 人間のミスを排除
- 継続的検証: 毎回同じ手順
これらを実践することで、**「テストが通れば本当に動く」**真のTDD環境を構築できます。
参考リンク
この記事が同じような問題に悩む開発者の助けになれば幸いです。
Discussion