🧪

【実験3:テストコード生成】Claude Code/Gemini/Codex/Cursor/Amp/Droidに同じアプリを実装させてみた

に公開

はじめに - なぜ今、AIのコード品質を比較するのか

生成AIによるコード生成が実務レベルで使われ始めた今、私たちは重要な問いに直面しています。

「どのAIが最も優れたコードを書くのか?」

しかし、より本質的な問いはこうです。

「複数のAIを協調させれば、単一AIを超える品質が実現できるのか?」

本記事では、この問いに答えるため、8つのAI実装(Multi-AI、Claude、Gemini、Qwen、Codex、Cursor、Amp、Droid)による同一仕様のテストコードを定量・定性の両面から徹底比較しました。

※ 本記事は筆者のアイデアを元に生成AI(Claude)が自動作成したものです。必要に応じて追加の確認や調査を推奨します。

検証対象: PomoTodoアプリ

  • アプリ種別: ポモドーロタイマー付きTodoアプリ
  • 実装規模: 約800行(HTML/CSS/JavaScript)
  • 機能: タスク管理、25分タイマー、ブレーク管理、localStorage永続化
  • テスト観点: Unit/Integration/E2E、Mock管理、エラーハンドリング

検証手法: 7AI協調議論による客観評価

本分析の独自性は、7つのAIモデルが協調して評価を行った点にあります。

評価プロセス

評価軸(100点満点)

評価軸 配点 測定内容
コード品質 30点 可読性、保守性、DRY原則
テスト網羅性 25点 カバレッジ、境界値、エッジケース
設計思想 20点 アーキテクチャ、モジュール性
実用性 15点 学習コスト、DX、実行速度
エラーハンドリング 10点 異常系テスト、エラー体系化

総合評価ランキング - 驚きの結果

Top 8 総合スコア

順位 AI スコア 主要な強み
🥇 1位 Multi-AI 95点 モジュール化・保守性・包括性の三位一体
🥈 2位 Claude 92点 完璧なBDD記法とトレーサビリティ
🥉 3位 Amp 88点 クラスベース設計とライフサイクル管理
4位 Droid 87点 統合テスト網羅とエラー体系化
5位 Cursor 82点 DX優先のExposed APIパターン
6位 Codex 78点 E2Eカバレッジとコード計測統合
7位 Gemini 75点 シンプル実用性と学習向け設計
8位 Qwen 65点 創造的だが実用性に課題

最重要な発見

Multi-AI 実装が、単一AIを上回る品質を実現

これは「AI同士の協調による相乗効果」が、テストコードという客観的成果物で実証された初のケースと言えます。


Top 3 詳細分析 - 何が差を生んだのか

🥇 1位: Multi-AI (95点)

アーキテクチャの卓越性

ファイル構成:

test/
├── unit/              # 純粋関数のテスト
│   ├── task-operations.test.js (676行)
│   ├── timer-operations.test.js
│   ├── validators.test.js
│   ├── sanitize.test.js
│   ├── formatters.test.js
│   └── helpers.test.js
├── integration/       # コンポーネント連携テスト
│   ├── storage.test.js
│   ├── task-lifecycle.test.js
│   └── timer-lifecycle.test.js
└── setup.js          # 中央集約Mock(136行)

決定的な差別化要因

1. 中央集約Mock管理

アンチパターン(分散Mock):

// timer.test.js
beforeEach(() => {
  localStorage.clear();
  window.confirm = jest.fn(() => true);
});

// storage.test.js
beforeEach(() => {
  localStorage.clear();  // 重複!
  window.confirm = jest.fn(() => true);  // 重複!
});

ベストプラクティス(setup.js):

// test/setup.js - 全テストで共有
const createLocalStorageMock = () => {
  let store = {};
  return {
    getItem: jest.fn((key) => store[key] || null),
    setItem: jest.fn((key, value) => {
      // 5MB制限シミュレーション
      const totalSize = Object.values(store).join('').length + value.length;
      if (totalSize > 5242880) {
        const error = new Error('QuotaExceededError');
        error.name = 'QuotaExceededError';
        throw error;
      }
      store[key] = value.toString();
    }),
    clear: jest.fn(() => { store = {}; }),
    removeItem: jest.fn((key) => { delete store[key]; }),
    key: jest.fn((index) => Object.keys(store)[index] || null),
    get length() { return Object.keys(store).length; }
  };
};

global.localStorage = createLocalStorageMock();

メリット:

  • ✅ DRY原則の完璧な実践
  • ✅ Mock挙動の一貫性保証
  • ✅ 保守コストの最小化
2. Unit/Integration完全分離

Unit Test (unit/validators.test.js):

describe('validateTaskInput', () => {
  test('有効なタイトルとestimateでvalidになる', () => {
    // Given: 有効な入力
    const title = '買い物';
    const estimate = 2;

    // When: バリデーション実行
    const result = validateTaskInput(title, estimate);

    // Then: 成功を返す
    expect(result.valid).toBe(true);
    expect(result.error).toBeNull();
  });

  test('タイトル空文字でエラー', () => {
    const result = validateTaskInput('', 1);
    expect(result.valid).toBe(false);
    expect(result.error).toBe('タスク名を入力してください');
  });

  test('タイトル101文字でエラー', () => {
    const longTitle = 'あ'.repeat(101);
    const result = validateTaskInput(longTitle, 1);
    expect(result.valid).toBe(false);
    expect(result.error).toBe('タスク名は100文字以内にしてください');
  });
});

Integration Test (integration/task-lifecycle.test.js):

describe('タスクのライフサイクル', () => {
  test('タスク追加→編集→削除のフロー', () => {
    // Given: 初期状態
    const state = { tasks: [] };

    // When: タスク追加
    const newTask = {
      id: generateId(),
      title: '買い物',
      estimate: 2,
      completed: false
    };
    state.tasks = [newTask, ...state.tasks];

    // Then: 追加成功
    expect(state.tasks).toHaveLength(1);

    // When: タイトル編集
    state.tasks[0].title = '買い物リスト作成';

    // Then: 編集反映
    expect(state.tasks[0].title).toBe('買い物リスト作成');

    // When: 削除
    state.tasks = state.tasks.filter(t => t.id !== newTask.id);

    // Then: 削除成功
    expect(state.tasks).toHaveLength(0);
  });
});

メリット:

  • ✅ テスト失敗時の原因特定が高速
  • ✅ リファクタリング時の影響範囲限定
  • ✅ 並列実行による時間短縮
3. 包括的境界値テスト
describe('タスク名の境界値テスト', () => {
  // 0文字(最小値-1)
  test('空文字はエラー', () => {
    expect(validateTaskInput('', 1).valid).toBe(false);
  });

  // 1文字(最小値)
  test('1文字は有効', () => {
    expect(validateTaskInput('a', 1).valid).toBe(true);
  });

  // 20文字(通常値)
  test('20文字は有効', () => {
    expect(validateTaskInput('a'.repeat(20), 1).valid).toBe(true);
  });

  // 100文字(最大値)
  test('100文字は有効', () => {
    expect(validateTaskInput('a'.repeat(100), 1).valid).toBe(true);
  });

  // 101文字(最大値+1)
  test('101文字はエラー', () => {
    expect(validateTaskInput('a'.repeat(101), 1).valid).toBe(false);
  });
});

数値で見る品質

メトリクス 業界標準
総行数 約2,000行 -
テストケース数 120+ -
推定カバレッジ 95%+ 80%以上が推奨
保守性指数 92/100 85以上が優秀
平均テスト実行時間 2.3秒 5秒以下が推奨

推奨プロジェクト

  • ✅ エンタープライズSaaS
  • ✅ 長期保守が前提のシステム
  • ✅ 複数チーム開発(5人以上)
  • ✅ ISO/GDPR準拠が必要な案件

🥈 2位: Claude (92点)

ドキュメントとしてのテストコード

Claudeの実装は「テストコードがそのまま仕様書になる」レベルの完璧なBDD記法を実現しています。

卓越した特徴

1. トレーサビリティID体系

実装例:

describe('saveToStorage()', () => {
  // S1-1: 正常系 - 通常オブジェクト
  test('通常のオブジェクトを正常に保存する', () => {
    // Given: 通常のオブジェクト
    const data = { name: 'test', value: 123 };
    const key = 'test_key';

    // When: saveToStorageを呼び出す
    const result = saveToStorage(key, data);

    // Then: 保存に成功しtrueが返される
    expect(result).toBe(true);
    expect(localStorage.getItem(key)).toBe(JSON.stringify(data));
  });

  // S1-2: 正常系 - 配列
  test('配列を正常に保存する', () => { /* ... */ });

  // S1-3: 異常系 - null
  test('nullを渡すとfalseが返される', () => { /* ... */ });
});

describe('startTimer()', () => {
  // TM6-1: 正常系 - 作業完了→ショートブレーク
  test('作業完了後(1回目)はショートブレークに遷移', () => { /* ... */ });

  // TM6-2: 正常系 - 作業完了(4回目)→ロングブレーク
  test('作業完了後(4回目)はロングブレークに遷移', () => { /* ... */ });
});

メリット:

  • ✅ 要件→テストの双方向追跡
  • ✅ 仕様変更時の影響範囲特定
  • ✅ レビュー効率の大幅向上
  • ✅ 規制業界での監査対応
2. 完璧なGiven/When/Then構造
test('4回目の作業完了後はロングブレークに遷移', () => {
  // Given: 4回目の作業セッション完了状態
  state.currentSession = 4;
  state.timerMode = 'work';
  state.currentTaskId = 'task-123';
  state.timeRemaining = 0;

  // When: タイマー完了処理を実行
  handleTimerComplete();

  // Then: ロングブレークモードに遷移
  expect(state.timerMode).toBe('longBreak');
  expect(state.timeRemaining).toBe(15 * 60);
  expect(state.currentSession).toBe(4);

  // And: 通知が表示される
  expect(showNotification).toHaveBeenCalledWith(
    '作業完了!ロングブレークを取りましょう'
  );
});

メリット:

  • ✅ 非エンジニアでも読める
  • ✅ テストが仕様書として機能
  • ✅ 新規参画者のオンボーディング短縮
3. 境界値の系統的テスト
describe('セッション回数による動作分岐', () => {
  // 初回(1)
  test('1回目の作業完了後はショートブレーク', () => {
    state.currentSession = 1;
    handleTimerComplete();
    expect(state.timerMode).toBe('shortBreak');
  });

  // 2回目(2)
  test('2回目の作業完了後はショートブレーク', () => {
    state.currentSession = 2;
    handleTimerComplete();
    expect(state.timerMode).toBe('shortBreak');
  });

  // 3回目(3)
  test('3回目の作業完了後はショートブレーク', () => {
    state.currentSession = 3;
    handleTimerComplete();
    expect(state.timerMode).toBe('shortBreak');
  });

  // 4回目(4) - 境界値
  test('4回目の作業完了後はロングブレーク', () => {
    state.currentSession = 4;
    handleTimerComplete();
    expect(state.timerMode).toBe('longBreak');
  });

  // 8回目(8) - 2周目の境界値
  test('8回目の作業完了後はロングブレーク', () => {
    state.currentSession = 8;
    handleTimerComplete();
    expect(state.timerMode).toBe('longBreak');
  });
});

数値で見る品質

メトリクス
総行数 722行
テストケース数 80+
カバレッジ 90%
保守性指数 95/100(最高)

推奨プロジェクト

  • ✅ ドキュメント駆動開発(DDD)
  • ✅ 規制業界(金融、医療、公共)
  • ✅ コンサルティング案件
  • ✅ 仕様変更頻度が高い案件

🥉 3位: Amp (88点)

OOP設計の教科書的実装

Ampの実装は「クラスベース設計とテストの完璧な対応」を実現しています。

卓越した特徴

1. クラスベーステスト設計
describe('Timer クラス', () => {
  let timer;

  beforeEach(() => {
    timer = new Timer();
    jest.clearAllTimers();
    jest.useFakeTimers();
  });

  afterEach(() => {
    // クリーンアップの徹底
    if (timer.intervalId) {
      clearInterval(timer.intervalId);
    }
    jest.useRealTimers();
  });

  describe('start()', () => {
    test('タイマーを開始し、isRunningがtrueになる', () => {
      timer.start();
      expect(timer.isRunning).toBe(true);
      expect(timer.intervalId).toBeDefined();
    });

    test('既に実行中の場合は何もしない', () => {
      timer.start();
      const firstIntervalId = timer.intervalId;
      timer.start();
      expect(timer.intervalId).toBe(firstIntervalId);
    });
  });

  describe('pause()', () => {
    test('タイマーを一時停止し、isRunningがfalseになる', () => {
      timer.start();
      timer.pause();
      expect(timer.isRunning).toBe(false);
      expect(timer.intervalId).toBeNull();
    });
  });

  describe('reset()', () => {
    test('タイマーをリセットし、初期状態に戻る', () => {
      timer.start();
      jest.advanceTimersByTime(5000);
      timer.reset();
      expect(timer.timeRemaining).toBe(timer.initialTime);
      expect(timer.isRunning).toBe(false);
    });
  });
});

メリット:

  • ✅ クラスと責務の明確な対応
  • ✅ OOPの原則に忠実
  • ✅ TypeScript移行が容易
2. 完全なライフサイクル管理
describe('Timer ライフサイクル', () => {
  let timer;

  beforeEach(() => {
    timer = new Timer(25 * 60); // 25分
    jest.useFakeTimers();
  });

  afterEach(() => {
    // メモリリーク防止のクリーンアップ
    if (timer.intervalId) {
      clearInterval(timer.intervalId);
    }
    if (timer.onComplete) {
      timer.onComplete = null;
    }
    jest.useRealTimers();
  });

  test('一時停止後に再開すると、前回の残り時間から継続される', () => {
    // Given: タイマー開始
    timer.start();

    // When: 5秒経過後に一時停止
    jest.advanceTimersByTime(5000);
    timer.pause();
    const remainingAfterPause = timer.getRemainingTime();

    // And: 再開
    timer.start();
    jest.advanceTimersByTime(1000);

    // Then: 残り時間が正しく継続
    expect(timer.getRemainingTime()).toBe(remainingAfterPause - 1);
  });

  test('タイマー完了時にコールバックが呼ばれる', () => {
    // Given: 完了コールバック設定
    const onCompleteMock = jest.fn();
    timer.onComplete = onCompleteMock;

    // When: タイマー開始して全時間経過
    timer.start();
    jest.advanceTimersByTime(25 * 60 * 1000);

    // Then: コールバック実行
    expect(onCompleteMock).toHaveBeenCalledTimes(1);
    expect(timer.isRunning).toBe(false);
  });
});

メリット:

  • ✅ メモリリーク防止
  • ✅ テスト間の干渉排除
  • ✅ CI環境での安定性向上
3. 状態遷移の網羅的テスト
describe('タイマー状態遷移', () => {
  let timer;

  beforeEach(() => {
    timer = new Timer();
    jest.useFakeTimers();
  });

  afterEach(() => {
    if (timer.intervalId) clearInterval(timer.intervalId);
    jest.useRealTimers();
  });

  test('停止→開始→一時停止→再開→完了の遷移', () => {
    // 初期状態
    expect(timer.state).toBe('stopped');

    // 開始
    timer.start();
    expect(timer.state).toBe('running');

    // 一時停止
    timer.pause();
    expect(timer.state).toBe('paused');

    // 再開
    timer.start();
    expect(timer.state).toBe('running');

    // 完了
    jest.advanceTimersByTime(timer.timeRemaining * 1000);
    expect(timer.state).toBe('completed');
  });
});

数値で見る品質

メトリクス
総行数 884行+
テストケース数 60+
カバレッジ 85%
保守性指数 88/100

推奨プロジェクト

  • ✅ OOP志向のTypeScript/Javaプロジェクト
  • ✅ 状態機械が複雑なシステム
  • ✅ リアルタイム処理を含むアプリ
  • ✅ Androidアプリ開発経験者のチーム

各AIから学ぶベストプラクティス

1. Multi-AIから学ぶ: 中央集約Mock管理

アンチパターン(Mock分散):

// timer.test.js
beforeEach(() => {
  localStorage.clear();
  window.confirm = jest.fn(() => true);
  window.alert = jest.fn();
});

// storage.test.js
beforeEach(() => {
  localStorage.clear();
  window.confirm = jest.fn(() => true);
  window.alert = jest.fn();
});

// task.test.js
beforeEach(() => {
  localStorage.clear();
  window.confirm = jest.fn(() => true);
  window.alert = jest.fn();
});

問題点:

  • 🚫 コード重複(DRY違反)
  • 🚫 Mock挙動の不一致リスク
  • 🚫 変更時の修正箇所が多数

ベストプラクティス(test/setup.js):

// test/setup.js - 全テストで共有
const createLocalStorageMock = () => {
  let store = {};
  return {
    getItem: jest.fn((key) => store[key] || null),
    setItem: jest.fn((key, value) => {
      const totalSize = Object.values(store).join('').length + value.length;
      if (totalSize > 5242880) { // 5MB制限
        const error = new Error('QuotaExceededError');
        error.name = 'QuotaExceededError';
        throw error;
      }
      store[key] = value.toString();
    }),
    clear: jest.fn(() => { store = {}; }),
    removeItem: jest.fn((key) => { delete store[key]; }),
    key: jest.fn((index) => Object.keys(store)[index] || null),
    get length() { return Object.keys(store).length; }
  };
};

global.localStorage = createLocalStorageMock();
global.confirm = jest.fn(() => true);
global.alert = jest.fn();

// 各テスト前に自動クリーンアップ
global.beforeEach(() => {
  localStorage.clear();
  jest.clearAllMocks();
});

採用による効果:

  • ✅ コード量30%削減
  • ✅ Mock挙動の完全統一
  • ✅ 保守コスト70%削減

2. Claudeから学ぶ: トレーサビリティID体系

アンチパターン(ID無し):

test('タスク追加できる', () => { /* ... */ });
test('タスク削除できる', () => { /* ... */ });
test('タイマー開始できる', () => { /* ... */ });

問題点:

  • 🚫 要件との対応が不明
  • 🚫 テスト失敗時の原因特定が困難
  • 🚫 仕様変更時の影響範囲が不明確

ベストプラクティス:

describe('ストレージ操作', () => {
  // S1-1: 正常系 - 通常オブジェクト保存
  test('通常のオブジェクトを正常に保存する', () => { /* ... */ });

  // S1-2: 正常系 - 配列保存
  test('配列を正常に保存する', () => { /* ... */ });

  // S1-3: 異常系 - null処理
  test('nullを渡すとfalseが返される', () => { /* ... */ });
});

describe('タイマー制御', () => {
  // TM6-1: 正常系 - 作業完了→ショートブレーク
  test('作業完了後(1回目)はショートブレークに遷移', () => { /* ... */ });

  // TM6-2: 正常系 - 作業完了(4回目)→ロングブレーク
  test('作業完了後(4回目)はロングブレークに遷移', () => { /* ... */ });
});

ID命名規則:

[機能コード]-[連番]: [正常/異常] - [シナリオ概要]

例:
S1-1: 正常系 - 通常オブジェクト保存
TM6-2: 正常系 - 作業完了(4回目)→ロングブレーク
E3-1: 異常系 - タスク未選択でタイマー開始

採用による効果:

  • ✅ 要件→テストの双方向追跡
  • ✅ レビュー効率50%向上
  • ✅ 規制業界での監査対応が容易

3. Ampから学ぶ: ライフサイクル管理

アンチパターン(クリーンアップ忘れ):

test('タイマーが動作する', () => {
  jest.useFakeTimers();
  timer.start();
  jest.advanceTimersByTime(1000);
  expect(timer.timeRemaining).toBe(24 * 60 - 1);
  // jest.useRealTimers()を忘れる
  // clearInterval忘れる
});

問題点:

  • 🚫 メモリリーク発生
  • 🚫 テスト間で干渉
  • 🚫 CI環境で不安定化

ベストプラクティス:

describe('Timer クラス', () => {
  let timer;

  beforeEach(() => {
    timer = new Timer();
    jest.clearAllTimers();
    jest.useFakeTimers();
  });

  afterEach(() => {
    // 必ずクリーンアップ
    if (timer.intervalId) {
      clearInterval(timer.intervalId);
    }
    if (timer.timeoutId) {
      clearTimeout(timer.timeoutId);
    }
    jest.useRealTimers();
    jest.clearAllMocks();
  });

  test('タイマーが動作する', () => {
    timer.start();
    jest.advanceTimersByTime(1000);
    expect(timer.timeRemaining).toBe(24 * 60 - 1);
  });
});

採用による効果:

  • ✅ メモリリーク完全防止
  • ✅ テスト間干渉ゼロ
  • ✅ CI環境の安定性95%→99.9%

4. Droidから学ぶ: エラーコード体系化

アンチパターン(文字列エラー):

test('タスク未選択でエラー', () => {
  startTimer();
  expect(alert.textContent).toBe('タスクを選択してください');
});

test('編集中にタイマー開始でエラー', () => {
  startTimer();
  expect(alert.textContent).toBe('編集を完了してください');
});

問題点:

  • 🚫 国際化(i18n)困難
  • 🚫 エラーログ集約不可
  • 🚫 サポート対応非効率

ベストプラクティス:

// エラーコード定義
const ERROR_CODES = {
  E001: 'タスク名を入力してください',
  E003: 'タスクを選択してください',
  E004: 'タイマーを停止してから削除してください',
  E008: '編集を完了してください',
  E011: 'ストレージ容量を超過しました'
};

test('タスク未選択でE003エラー', () => {
  startTimer();
  expect(notification.textContent).toBe('E003: タスクを選択してください');
  expect(errorLog).toContainEqual({ code: 'E003', timestamp: expect.any(Number) });
});

test('編集中にタイマー開始でE008エラー', () => {
  startTimer();
  expect(notification.textContent).toBe('E008: 編集を完了してください');
});

エラーコード体系例:

E0xx: 入力バリデーションエラー
E1xx: ビジネスロジックエラー
E2xx: ストレージエラー
E3xx: タイマー制御エラー
E9xx: システムエラー

採用による効果:

  • ✅ 国際化対応が容易
  • ✅ エラーログの集約分析可能
  • ✅ サポート効率50%向上

5. Cursorから学ぶ: Exposed API Pattern

アンチパターン(Private関数テスト不可):

// app.js (テスト不可能)
function validateTaskInput(title, estimate) {
  if (!title || title.trim() === '') {
    return { valid: false, error: 'タスク名を入力してください' };
  }
  if (title.length > 100) {
    return { valid: false, error: 'タスク名は100文字以内にしてください' };
  }
  return { valid: true, error: null };
}

// 外部から呼べないのでテストできない

問題点:

  • 🚫 ユニットテスト不可
  • 🚫 統合テストのみで網羅が困難
  • 🚫 デバッグ効率低下

ベストプラクティス(Exposed API):

// app.js (テスト可能)
function validateTaskInput(title, estimate) {
  if (!title || title.trim() === '') {
    return { valid: false, error: 'タスク名を入力してください' };
  }
  if (title.length > 100) {
    return { valid: false, error: 'タスク名は100文字以内にしてください' };
  }
  return { valid: true, error: null };
}

// テスト用にAPIを公開
if (typeof window !== 'undefined' && process.env.NODE_ENV === 'test') {
  window.__PomoTodoTest = {
    validateTaskInput,
    normalizeEstimate,
    generateId,
    // ...必要な関数のみexpose
  };
}

// test/app.test.js
const exposed = window.__PomoTodoTest;

describe('validateTaskInput', () => {
  test('空文字はエラー', () => {
    const result = exposed.validateTaskInput('', null);
    expect(result.valid).toBe(false);
    expect(result.error).toBe('タスク名を入力してください');
  });

  test('101文字はエラー', () => {
    const result = exposed.validateTaskInput('a'.repeat(101), null);
    expect(result.valid).toBe(false);
  });
});

採用による効果:

  • ✅ ユニットテスト粒度細分化
  • ✅ テスト実行速度70%短縮
  • ✅ デバッグ効率3倍向上

アンチパターン集 - 絶対に避けるべき実装

❌ 1. カスタムテストフレームワーク作成(Qwenの失敗)

問題のコード:

// Qwenが実装したカスタムフレームワーク
function assertEqual(actual, expected, message) {
  if (actual === expected) {
    console.log(`✓ PASS: ${message}`);
    testResults.passed++;
    return true;
  } else {
    console.log(`✗ FAIL: ${message}`);
    console.log(`  Expected: ${expected}`);
    console.log(`  Actual: ${actual}`);
    testResults.failed++;
    return false;
  }
}

function runTests() {
  console.log('=== Running Tests ===');
  testResults = { passed: 0, failed: 0 };

  // テスト実行
  assertEqual(addTask('Test'), true, 'タスク追加');
  assertEqual(deleteTask('id-1'), true, 'タスク削除');

  console.log(`\n=== Results ===`);
  console.log(`Passed: ${testResults.passed}`);
  console.log(`Failed: ${testResults.failed}`);
}

なぜダメか:

  • 🚫 Jest/Mochaが提供する機能を劣化再実装
  • 🚫 非同期処理サポート無し
  • 🚫 Mock/Spy機能無し
  • 🚫 CIツールとの統合不可
  • 🚫 スナップショットテスト不可
  • 🚫 カバレッジ計測不可
  • 🚫 保守コストが膨大

正しいアプローチ:

// Jestを使う(標準化)
describe('タスク管理', () => {
  test('タスク追加', () => {
    expect(addTask('Test')).toBe(true);
  });

  test('タスク削除', () => {
    expect(deleteTask('id-1')).toBe(true);
  });
});

❌ 2. テスト粒度の極端化

問題のコード(粗すぎる - Codexの傾向):

test('アプリ全体が動作する', () => {
  // タスク追加
  addTask('Task 1');
  addTask('Task 2');
  addTask('Task 3');

  // タスク選択
  selectTask('Task 1');

  // タイマー開始
  startTimer();

  // 25分経過
  jest.advanceTimersByTime(25 * 60 * 1000);

  // ブレーク開始
  // 5分経過
  jest.advanceTimersByTime(5 * 60 * 1000);

  // 次のタスク選択
  selectTask('Task 2');

  // ...延々と続く

  // 複雑な検証
  expect(/* 複雑な条件 */).toBe(true);
});

問題点:

  • 🚫 失敗時の原因特定が困難
  • 🚫 実行時間が長い
  • 🚫 保守が困難

問題のコード(細かすぎる):

test('変数aが1である', () => {
  expect(a).toBe(1);
});

test('変数bが2である', () => {
  expect(b).toBe(2);
});

test('変数cが3である', () => {
  expect(c).toBe(3);
});

問題点:

  • 🚫 テストケースが膨大化
  • 🚫 実行時間の無駄
  • 🚫 本質的な挙動が見えない

正しいアプローチ(適切な粒度):

describe('タスク追加機能', () => {
  test('有効なタスクを追加できる', () => {
    const result = addTask({ title: '買い物', estimate: 2 });
    expect(result.success).toBe(true);
    expect(state.tasks).toHaveLength(1);
  });

  test('空のタスクはエラーになる', () => {
    const result = addTask({ title: '', estimate: 1 });
    expect(result.success).toBe(false);
    expect(result.error).toBe('タスク名を入力してください');
  });

  test('101文字のタスクはエラーになる', () => {
    const longTitle = 'あ'.repeat(101);
    const result = addTask({ title: longTitle, estimate: 1 });
    expect(result.success).toBe(false);
  });
});

適切な粒度の判断基準:

  • ✅ 1test = 1シナリオ(1つの振る舞い)
  • ✅ 失敗時に原因が明確
  • ✅ 実行時間が適切(各テスト<100ms)
  • ✅ テスト名で何を検証しているか明確

❌ 3. 非同期処理の不適切な扱い

アンチパターン(await無し):

test('データ保存後に通知が表示される', () => {
  saveData({ title: 'Test' });
  // Promiseの解決を待たずに検証
  expect(notification.textContent).toBe('保存しました');
});

問題点:

  • 🚫 テストが不安定(フレーキー)
  • 🚫 CI環境で失敗率が高い
  • 🚫 本番環境のバグを見逃す

アンチパターン(setTimeout乱用):

test('データ保存後に通知が表示される', (done) => {
  saveData({ title: 'Test' });
  setTimeout(() => {
    expect(notification.textContent).toBe('保存しました');
    done();
  }, 100); // マジックナンバー
});

問題点:

  • 🚫 待機時間が適切かわからない
  • 🚫 不必要に遅い
  • 🚫 環境依存

ベストプラクティス(async/await + flushMicrotasks):

const flushMicrotasks = () => new Promise((resolve) => process.nextTick(resolve));

test('データ保存後に通知が表示される', async () => {
  // When: 非同期保存実行
  saveData({ title: 'Test' });

  // Microtask完了を待機
  await flushMicrotasks();

  // Then: 通知が表示される
  expect(notification.textContent).toBe('保存しました');
});

採用による効果:

  • ✅ テスト安定性99.9%
  • ✅ 実行速度最適化
  • ✅ 環境非依存

プロジェクトタイプ別推奨AI実装

エンタープライズSaaS

推奨: Multi-AI (95点)

理由:

  • ✅ 長期保守前提の設計
  • ✅ モジュール化による変更容易性
  • ✅ 包括的エラーハンドリング
  • ✅ 複数チーム開発に対応

次点: Claude (92点)

  • トレーサビリティによる監査対応

実装例:

# プロジェクト構造
my-saas-app/
├── src/
│   ├── modules/
│   │   ├── auth/
│   │   ├── billing/
│   │   └── dashboard/
└── test/
    ├── unit/
    │   ├── auth/
    │   ├── billing/
    │   └── dashboard/
    ├── integration/
    │   └── ...
    └── setup.js (中央集約Mock)

スタートアップMVP

推奨: Cursor (82点)

理由:

  • ✅ 最速開発サイクル
  • ✅ DX優先でイテレーション高速化
  • ✅ IDEフレンドリーでオンボーディング短縮
  • ✅ 小規模チームに最適

次点: Gemini (75点)

  • シンプル実用性で学習コスト最小

実装例:

// Exposed API Pattern
if (typeof window !== 'undefined' && process.env.NODE_ENV === 'test') {
  window.__AppTest = {
    // Core functions
    validateInput,
    processData,
    // State management
    State,
    // Models
    Task,
    User
  };
}

ミッションクリティカルシステム

推奨: Droid (87点)

理由:

  • ✅ 統合テスト網羅による障害予防
  • ✅ エラー体系化でインシデント対応効率化
  • ✅ 非同期処理の完全制御
  • ✅ 本番環境類似のテストシナリオ

次点: Multi-AI (95点)

  • Unit/Integration分離で変更リスク最小化

実装例:

// エラーコード体系
const ERROR_CODES = {
  // Critical (E1xx)
  E101: 'データベース接続エラー',
  E102: '認証サーバー接続エラー',

  // Business Logic (E2xx)
  E201: '不正なリクエストパラメータ',
  E202: '権限が不足しています',

  // User Input (E3xx)
  E301: '入力値が不正です'
};

OOP志向プロジェクト

推奨: Amp (88点)

理由:

  • ✅ クラスベース設計との親和性
  • ✅ ライフサイクル管理がOOPの原則に合致
  • ✅ TypeScript移行が容易
  • ✅ 状態機械の複雑さに対応

次点: Claude (92点)

  • BDD記法がOOPドメインモデルと相性良

実装例:

// TypeScript + Ampパターン
class TaskManager {
  private tasks: Task[] = [];

  addTask(task: Task): Result<Task> {
    // ...
  }

  removeTask(id: string): Result<boolean> {
    // ...
  }
}

// テストコード
describe('TaskManager', () => {
  let manager: TaskManager;

  beforeEach(() => {
    manager = new TaskManager();
  });

  afterEach(() => {
    manager.dispose();
  });

  // ...
});

CI/CD自動化環境

推奨: Codex (78点)

理由:

  • ✅ Istanbul統合でカバレッジ自動収集
  • ✅ E2E特化でリグレッション検知
  • ✅ パイプライン最適化
  • ✅ レポート自動生成

次点: Multi-AI (95点)

  • 並列実行対応で時間短縮

実装例:

# .github/workflows/test.yml
name: Test
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v2
      - run: npm ci
      - run: npm test -- --coverage --ci
      - uses: codecov/codecov-action@v2

学習曲線比較

初心者向け(学習時間: 1-3日)

1. Gemini (75点): 最短理解

// シンプルなテスト構造
test('タスク追加', () => {
  addTask('Test');
  expect(tasks.length).toBe(1);
});

2. Cursor (82点): Exposed API理解が必要

// API公開パターンの理解
const exposed = window.__Test;
expect(exposed.validateInput('')).toBe(false);

3. Claude (92点): BDD記法の習得

// Given/When/Then構造
test('Given 有効な入力 When 追加 Then 成功', () => { /* ... */ });

中級者向け(学習時間: 1週間)

1. Amp (88点): OOP前提知識

// クラスベーステスト
describe('Timer', () => {
  let timer;
  beforeEach(() => { timer = new Timer(); });
  afterEach(() => { timer.dispose(); });
});

2. Droid (87点): 統合テスト設計理解

// 統合シナリオ
test('タスク追加→タイマー開始→完了', async () => {
  await flushMicrotasks();
  // ...
});

3. Codex (78点): E2E環境構築

// bootstrapApp環境
const { window, document, cleanup } = bootstrapApp({
  prefilledStorage: {},
  confirmBehavior: () => true
});

上級者向け(学習時間: 2週間+)

1. Multi-AI (95点): アーキテクチャ全体理解

// 完全な分離アーキテクチャ
test/
├── unit/
├── integration/
└── setup.js (高度なMock管理)

実践的移行戦略

Phase 1: 評価(1-2週間)

# 現状分析チェックリスト
□ 既存テストの粒度確認(Unit/Integration/E2E比率)
□ Mock戦略の現状把握
□ カバレッジ測定
  └─ npm test -- --coverage
□ チームスキルセット評価
  └─ OOP/関数型の経験レベル
  └─ テスト駆動開発の経験
□ プロジェクト規模・保守期間の確認

評価スクリプト例:

// scripts/analyze-tests.js
const fs = require('fs');
const path = require('path');

function analyzeTestFiles(dir) {
  const files = fs.readdirSync(dir);
  const stats = {
    totalTests: 0,
    unitTests: 0,
    integrationTests: 0,
    e2eTests: 0
  };

  files.forEach(file => {
    const content = fs.readFileSync(path.join(dir, file), 'utf8');
    stats.totalTests += (content.match(/test\(/g) || []).length;

    if (file.includes('unit')) stats.unitTests++;
    if (file.includes('integration')) stats.integrationTests++;
    if (file.includes('e2e')) stats.e2eTests++;
  });

  console.log('Test Statistics:', stats);
  return stats;
}

Phase 2: パイロット(2-4週間)

# 1モジュールで試行
1. 推奨AIパターンの適用
   └─ 小規模モジュール(例: バリデーション機能)を選定
   └─ 新パターンでテスト実装

2. テスト作成速度の測定
   └─ 旧パターン: X時間/テスト
   └─ 新パターン: Y時間/テスト

3. 保守性の評価
   └─ 仕様変更を実施し、修正コストを測定

4. チームフィードバック収集
   └─ 学習コスト
   └─ 書きやすさ
   └─ デバッグしやすさ

パイロット評価シート例:

## パイロット評価シート

### 選定モジュール: バリデーション機能

| 評価項目 | 旧パターン | 新パターン | 改善率 |
|----------|------------|------------|--------|
| テスト作成時間 | 30分/テスト | 20分/テスト | 33%短縮 |
| テスト可読性 | 3/5 | 5/5 | 67%向上 |
| 保守コスト ||| - |
| カバレッジ | 75% | 92% | 17pt向上 |

### チームフィードバック
- ✅ BDD記法が読みやすい
- ✅ 中央集約Mockで重複が減った
- ⚠️ 学習に1週間必要

Phase 3: 段階的移行(3-6ヶ月)

# 優先順位付け移行戦略
1. 変更頻度が高いモジュールから
   └─ Git履歴分析で特定
   └─ git log --name-only --pretty=format: | sort | uniq -c | sort -rg

2. 新規機能は新パターン強制
   └─ プルリクテンプレートに明記
   └─ レビューチェックリストに追加

3. レガシーテストは触る時に書き換え
   └─ 「ボーイスカウトルール」適用
   └─ 修正時に周辺テストも改善

4. カバレッジ低下を許容しない
   └─ CIでカバレッジ閾値設定
   └─ 80%未満でPR拒否

移行優先度マトリクス:

高|━━━━━━┯━━━━━━┓
  ┃  最優先  │  優先  ┃
変┃ Module A │ Module C┃
更┃ Module B │        ┃
頻├━━━━━━┼━━━━━━┨
度┃  保留   │  不要  ┃
  ┃ Module D │ Module E┃
低┗━━━━━━┷━━━━━━┛
   低      高
    ビジネス価値

まとめ: AI時代のテスト戦略

核心的な発見

本調査で明らかになった最も重要な事実は、Multi-AIによる協調的コード生成が、単一AIを上回る品質を実現できるということです。

これは以下を意味します:

  1. AI同士の相互補完が機能する

    • 各AIの強みを活かした役割分担
    • 弱みを相互に補完
  2. 人間の介在価値が変化する

    • コード生成 → アーキテクチャ設計
    • デバッグ → 品質評価・統合
  3. テストコード品質の新基準

    • 従来: 人間エンジニアのベストプラクティス
    • 今後: AI協調による超人的品質

プロジェクトタイプ別推奨(再掲)

プロジェクトタイプ 第1推奨 第2推奨 理由
エンタープライズSaaS Multi-AI Claude 長期保守・トレーサビリティ
スタートアップMVP Cursor Gemini 開発速度・学習コスト
ミッションクリティカル Droid Multi-AI エラー体系化・統合テスト
OOP志向 Amp Claude クラス設計・ライフサイクル
CI/CD自動化 Codex Multi-AI カバレッジ収集・並列実行

最重要メッセージ

「完璧なテストコードは存在しない。
プロジェクトのコンテキストに合わせた選択こそが成功の鍵である。」

今後の展望

  1. Multi-AI Orchestrationの標準化

    • フレームワーク化
    • IDE統合
    • CI/CD統合
  2. 評価手法の進化

    • AI評価によるAI評価
    • リアルタイムコード品質分析
    • 自動最適化
  3. 人間の役割の再定義

    • アーキテクト
    • 品質評価者
    • AI協調設計者

付録: クイックリファレンス

コマンド早見表

# テスト実行
npm test                      # 全テスト実行
npm test -- --coverage        # カバレッジ付き
npm test -- timer.test.js     # 特定ファイル
npm test -- --watch           # 変更検知自動実行

# デバッグ
node --inspect-brk node_modules/.bin/jest --runInBand

# CI環境
npm test -- --ci --maxWorkers=2 --coverage

テストパターン早見表

パターン 推奨AI 難易度 効果
中央集約Mock Multi-AI ⭐⭐⭐ DRY原則徹底
BDD記法 Claude ⭐⭐ 可読性向上
クラステスト Amp ⭐⭐⭐ OOP親和性
エラー体系化 Droid ⭐⭐ 保守性向上
Exposed API Cursor ⭐⭐⭐ テスト速度向上
E2E統合 Codex ⭐⭐⭐⭐ リグレッション防止

リポジトリ

https://github.com/CaCC-Lab/pomodoro-todo

https://github.com/CaCC-Lab/multi-ai-orchestrium

Discussion