テスト戦略ガイド

テスト戦略ガイド

概要

Miyabi の包括的なテスト戦略を説明します。単体テスト、統合テスト、E2E テストの実装方法、カバレッジ目標、CI/CD 統合について学びます。

対象読者

  • QA エンジニア
  • テストエンジニア
  • 開発者
  • テックリード

所要時間

  • 読了時間: 20 分
  • セットアップ時間: 30-45 分

前提知識

  • TypeScript の基礎
  • テストフレームワーク(Vitest/Jest)の知識
  • E2E テスト(Playwright)の基礎
  • CI/CD の概念

テスト構造

ディレクトリ構成

tests/
├── unit/                    # 単体テスト
│   ├── agents/
│   │   ├── coordinator.test.ts
│   │   ├── codegen.test.ts
│   │   └── review.test.ts
│   └── utils/
│       └── helpers.test.ts
├── integration/             # 統合テスト
│   ├── system.test.ts       # システム統合
│   ├── workflow.test.ts     # ワークフロー統合
│   └── api.test.ts          # API 統合
└── e2e/                     # E2E テスト
    ├── workflow.test.ts     # エンドツーエンドワークフロー
    ├── parallel.test.ts     # 並行処理
    └── cost-control.test.ts # コスト管理

テストの種類

1. 単体テスト

コンポーネント単位のテストです。

// tests/unit/agents/coordinator.test.ts
import { describe, test, expect } from 'vitest';
import { CoordinatorAgent } from '@/agents/coordinator';

describe('CoordinatorAgent', () => {
  test('should decompose issue into tasks', async () => {
    const coordinator = new CoordinatorAgent();
    const issue = {
      number: 270,
      title: 'Add user authentication',
      body: 'Implement login and registration'
    };

    const tasks = await coordinator.decomposeTask(issue);

    expect(tasks).toHaveLength(3);
    expect(tasks[0].title).toContain('login');
    expect(tasks[1].title).toContain('registration');
  });

  test('should detect circular dependencies', async () => {
    const coordinator = new CoordinatorAgent();
    const tasks = [
      { id: '1', dependencies: ['2'] },
      { id: '2', dependencies: ['1'] }  // 循環依存
    ];

    expect(() => coordinator.buildDAG(tasks))
      .toThrow('Circular dependency detected');
  });
});

2. 統合テスト

複数コンポーネントの連携をテストします。

// tests/integration/system.test.ts
import { describe, test, expect, beforeAll, afterAll } from 'vitest';
import { TaskOrchestrator } from '@/core/orchestrator';
import { WorkerRegistry } from '@/core/worker-registry';
import { LockManager } from '@/core/lock-manager';

describe('System Integration', () => {
  let orchestrator: TaskOrchestrator;
  let workerRegistry: WorkerRegistry;
  let lockManager: LockManager;

  beforeAll(() => {
    orchestrator = new TaskOrchestrator();
    workerRegistry = new WorkerRegistry();
    lockManager = new LockManager();
  });

  afterAll(async () => {
    await lockManager.cleanupExpiredLocks();
  });

  test('complete task lifecycle', async () => {
    // 1. タスク作成
    const task = {
      id: 'task-1',
      type: 'codegen',
      affectedFiles: ['src/auth.ts']
    };
    orchestrator.addTask(task);

    // 2. ワーカー登録
    const worker = workerRegistry.register({
      id: 'worker-1',
      capabilities: ['codegen']
    });

    // 3. タスククレーム
    const claimed = await orchestrator.claimTask(worker.id, task.id);
    expect(claimed.success).toBe(true);

    // 4. ロック取得
    const locked = await lockManager.acquireLocks(
      task.id,
      worker.id,
      task.affectedFiles
    );
    expect(locked.success).toBe(true);

    // 5. タスク実行
    await orchestrator.startTask(task.id);
    await orchestrator.completeTask(task.id, true);

    // 6. ロック解放
    await lockManager.releaseLocks(task.id);

    // 7. 検証
    const finalTask = orchestrator.getTask(task.id);
    expect(finalTask.status).toBe('completed');
  });

  test('should detect file conflicts', async () => {
    const task1 = {
      id: 'task-1',
      affectedFiles: ['src/auth.ts']
    };
    const task2 = {
      id: 'task-2',
      affectedFiles: ['src/auth.ts']  // 同じファイル
    };

    const worker1 = workerRegistry.register({ id: 'worker-1' });
    const worker2 = workerRegistry.register({ id: 'worker-2' });

    // Worker 1 がロック取得
    await lockManager.acquireLocks(task1.id, worker1.id, task1.affectedFiles);

    // Worker 2 は失敗するはず
    const result = await lockManager.acquireLocks(
      task2.id,
      worker2.id,
      task2.affectedFiles
    );

    expect(result.success).toBe(false);
    expect(result.error).toContain('conflict');
  });
});

3. E2E テスト

エンドツーエンドのワークフローをテストします。

// tests/e2e/workflow.test.ts
import { describe, test, expect } from 'vitest';
import { simulateIssueCreation } from './helpers';

describe('E2E Workflow', () => {
  test('Zero-Learning-Cost: Issue → Label → Agent → PR', async () => {
    // 1. ユーザーが Issue を作成
    const issue = await simulateIssueCreation({
      title: 'Add feature',
      body: 'New feature description'
    });

    // 2. AI が自動ラベリング
    const labels = await waitForLabels(issue.number);
    expect(labels).toContain('type:feature');

    // 3. Agent が自動割り当て
    const state = await waitForState(issue.number);
    expect(state.agent).toBe('agent:codegen');

    // 4. PR が自動作成
    const pr = await waitForPR(issue.number);
    expect(pr).toBeDefined();
    expect(pr.state).toBe('draft');
  });

  test('Parallel Work: Multiple agents without conflicts', async () => {
    // 複数の Issue を同時作成
    const issues = await Promise.all([
      simulateIssueCreation({ title: 'Feature A' }),
      simulateIssueCreation({ title: 'Feature B' }),
      simulateIssueCreation({ title: 'Feature C' })
    ]);

    // すべて並行処理される
    const results = await Promise.all(
      issues.map(issue => waitForCompletion(issue.number))
    );

    // すべて成功
    expect(results.every(r => r.success)).toBe(true);

    // 競合なし
    expect(results.some(r => r.hadConflict)).toBe(false);
  });
});

テストの実行

基本コマンド

# すべてのテストを実行
npm test

# ウォッチモード
npm test -- --watch

# 特定のテストファイル
npm test tests/integration/system.test.ts

# カバレッジレポート
npm test -- --coverage

# 詳細モード
npm test -- --reporter=verbose

種類別実行

# 単体テストのみ
npm run test:unit

# 統合テストのみ
npm run test:integration

# E2E テストのみ
npm run test:e2e

# すべて順次実行
npm run test:all

カバレッジ目標

目標値

coverage_targets:
  overall: 80%       # 全体カバレッジ
  core: 90%          # コアコンポーネント
  critical: 100%     # クリティカルパス

  statements: 80%
  branches: 78%
  functions: 85%
  lines: 83%

カバレッジレポートの確認

# HTML レポート生成
npm test -- --coverage --reporter=html

# レポートを開く
open coverage/index.html

# コマンドラインで確認
npm test -- --coverage --reporter=text

CI/CD 統合

GitHub Actions

# .github/workflows/test.yml
name: Tests

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - 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: Generate coverage
        run: npm test -- --coverage

      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v3
        with:
          files: ./coverage/coverage-final.json

      - name: Check coverage threshold
        run: |
          COVERAGE=$(cat coverage/coverage-summary.json | jq '.total.lines.pct')
          if (( $(echo "$COVERAGE < 80" | bc -l) )); then
            echo "Coverage $COVERAGE% is below 80%"
            exit 1
          fi

テスト合格基準

必須条件

passing_criteria:
  all_tests_pass: true
  min_coverage: 80%
  no_flaky_tests: true
  execution_time: < 30s

  test_breakdown:
    unit_tests: 40+ passed
    integration_tests: 15+ passed
    e2e_tests: 7+ passed

  quality_gates:
    - ESLint: 0 errors
    - TypeScript: 0 errors
    - Security scan: 0 critical

レポート例

Test Suites: 3 passed, 3 total
Tests:       62 passed, 62 total
Snapshots:   0 total
Time:        28.5s

Coverage:
  Statements   : 82.3% ( 450/547 )
  Branches     : 78.1% ( 220/282 )
  Functions    : 85.7% ( 150/175 )
  Lines        : 83.2% ( 425/511 )

✅ All quality gates passed

テスト作成ガイド

テンプレート

import { describe, test, expect, beforeAll, afterAll } from 'vitest';

describe('Component Name', () => {
  beforeAll(() => {
    // テスト前のセットアップ
  });

  afterAll(() => {
    // テスト後のクリーンアップ
  });

  test('should do something', () => {
    // Arrange(準備)
    const input = { ... };

    // Act(実行)
    const result = doSomething(input);

    // Assert(検証)
    expect(result).toBe(expected);
  });
});

ベストプラクティス

  1. 説明的なテスト名
// ✅ 良い例
test('should throw error when input is invalid', () => {...});

// ❌ 悪い例
test('test 1', () => {...});
  1. AAA パターン
test('example', () => {
  // Arrange: テストデータを準備
  const input = createTestData();

  // Act: 関数を実行
  const result = targetFunction(input);

  // Assert: 結果を検証
  expect(result).toEqual(expected);
});
  1. テストの独立性
// ✅ 良い例: 各テストが独立
test('test 1', () => {
  const data = createFreshData();
  // テスト実行
});

test('test 2', () => {
  const data = createFreshData();
  // テスト実行
});

// ❌ 悪い例: テスト間で状態を共有
let sharedData;
test('test 1', () => {
  sharedData = createData();
});
test('test 2', () => {
  // sharedData に依存
});
  1. モックの活用
import { vi } from 'vitest';

test('should call external API', async () => {
  // 外部依存をモック
  const mockFetch = vi.fn().mockResolvedValue({ ok: true });
  global.fetch = mockFetch;

  await fetchData();

  expect(mockFetch).toHaveBeenCalledWith('https://api.example.com');
});
  1. クリーンアップ
afterAll(async () => {
  // テストデータを削除
  await cleanupTestDatabase();

  // 一時ファイルを削除
  await removeTemporaryFiles();

  // ロックを解放
  await lockManager.cleanupExpiredLocks();
});

トラブルシューティング

問題 1: テストがタイムアウトする

解決方法:

// vitest.config.ts
export default {
  test: {
    timeout: 10000,  // 10秒に延長
  },
};

// または個別のテストで
test('slow test', async () => {
  // テスト実行
}, { timeout: 20000 });

問題 2: ロックファイルが残る

解決方法:

afterAll(async () => {
  // 強制クリーンアップ
  await lockManager.cleanupAllLocks();

  // またはファイルシステムから削除
  await fs.rm('.locks', { recursive: true, force: true });
});

問題 3: 並列テストの干渉

解決方法:

// ユニークな ID を使用
test('parallel test', () => {
  const taskId = `test-task-${Date.now()}-${Math.random()}`;
  const task = createTask(taskId);
  // テスト実行
});

// または順次実行に変更
// vitest.config.ts
export default {
  test: {
    pool: 'forks',
    poolOptions: {
      forks: {
        singleFork: true  // 順次実行
      }
    }
  }
};

問題 4: カバレッジが低い

解決方法:

# カバレッジが低いファイルを特定
npm test -- --coverage --reporter=text

# 特定のファイルのテストを追加
# tests/unit/uncovered-file.test.ts を作成

# カバレッジを再確認
npm test -- --coverage

パフォーマンス最適化

1. テストの並列実行

// vitest.config.ts
export default {
  test: {
    pool: 'threads',
    poolOptions: {
      threads: {
        maxThreads: 4,  // 最大スレッド数
        minThreads: 1
      }
    }
  }
};

2. セットアップの共有

// tests/setup.ts
import { beforeAll, afterAll } from 'vitest';

let sharedResource;

beforeAll(async () => {
  // 重い初期化処理を1回だけ実行
  sharedResource = await expensiveSetup();
});

afterAll(async () => {
  await cleanup(sharedResource);
});

export { sharedResource };

3. スナップショットテストの活用

test('should render correctly', () => {
  const component = render(<MyComponent />);
  expect(component).toMatchSnapshot();
});

関連ドキュメント

次のステップ

  1. Agent テスト の詳細を学ぶ
  2. E2E テスト を強化する
  3. モニタリング を設定する

最終更新: 2025-10-10 バージョン: 2.0.0 ソース: TESTING_GUIDE.md メンテナー: Miyabi QA Team