📝

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で「テストは通るのに動かない」問題は、テスト環境と現実の乖離が根本原因です。

解決の鍵は:

  1. 環境の一致: テスト ≈ 開発 ≈ 本番
  2. 完全なカバレッジ: 単体 + 統合 + E2E
  3. 自動化: 人間のミスを排除
  4. 継続的検証: 毎回同じ手順

これらを実践することで、**「テストが通れば本当に動く」**真のTDD環境を構築できます。


参考リンク

この記事が同じような問題に悩む開発者の助けになれば幸いです。

Discussion