テスト戦略ガイド
概要
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);
});
});
ベストプラクティス
- 説明的なテスト名
// ✅ 良い例
test('should throw error when input is invalid', () => {...});
// ❌ 悪い例
test('test 1', () => {...});
- AAA パターン
test('example', () => {
// Arrange: テストデータを準備
const input = createTestData();
// Act: 関数を実行
const result = targetFunction(input);
// Assert: 結果を検証
expect(result).toEqual(expected);
});
- テストの独立性
// ✅ 良い例: 各テストが独立
test('test 1', () => {
const data = createFreshData();
// テスト実行
});
test('test 2', () => {
const data = createFreshData();
// テスト実行
});
// ❌ 悪い例: テスト間で状態を共有
let sharedData;
test('test 1', () => {
sharedData = createData();
});
test('test 2', () => {
// sharedData に依存
});
- モックの活用
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');
});
- クリーンアップ
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();
});
関連ドキュメント
- CodeGenAgent ガイド - テスト自動生成
- ReviewAgent ガイド - 品質チェック
- CI/CD パイプライン
- デバッグガイド
次のステップ
最終更新: 2025-10-10 バージョン: 2.0.0 ソース: TESTING_GUIDE.md メンテナー: Miyabi QA Team