「テストを書く時間がない」——個人開発でもチーム開発でも、この悩みは尽きません。機能を追加するたびにテストが追いつかず、リグレッションに怯えながらデプロイします。私自身、まさにその繰り返しでした。
Antigravity のエージェント機能を使い始めてから、テストに対する考え方が根本的に変わりました。AI にテストを「書かせる」のではなく、AI にテストパイプライン全体を「設計させる」アプローチに切り替えたところ、カバレッジは 30% から 85% に跳ね上がり、しかもテストの保守コストはほとんど増えませんでした。
ここではAntigravity のマルチエージェントを使って本番品質のテストパイプラインをゼロから構築する方法を、実装コードとともに共有します。
AI テスト生成が「使えない」と感じる本当の理由
AI にテストを書かせて「微妙だな」と感じた経験、ありませんか。私も最初はそうでした。原因を分析してみると、大きく3つのパターンに集約されます。
1. コンテキスト不足で的外れなテストが生成される
AI がコードの表面的な構造だけを見て、ビジネスロジックの意図を理解できていない状態です。たとえば、ユーザー認証の関数に対して「引数に null を渡したら例外が出る」というテストは書けても、「JWT の有効期限切れ後にリフレッシュトークンで再認証できるか」というシナリオまでは到達しません。
2. テスト同士の依存関係が考慮されない
AI は個々のテストケースを独立に生成しがちです。しかし本番では、テストの実行順序やデータベースの状態管理が品質を左右します。
3. 生成されたテストの「正しさ」を誰も検証しない
これが最も深刻です。AI が生成したテストがグリーンになっても、そのテスト自体が正しいかどうかは別問題です。アサーションが甘すぎるテストは、バグを見逃すだけでなく、誤った安心感を与えます。
この3つを解決するために、私は Antigravity のエージェントを3層構成にしました。
3層テストエージェントアーキテクチャ
テストパイプラインを「生成」「検証」「実行」の3つのエージェントに分離する設計です。それぞれが独立した責務を持ち、互いの出力を検証し合うことで、単体の AI が見逃すミスを補います。
// agents/test-pipeline/orchestrator.ts
// テストパイプライン全体を統括するオーケストレーター
interface TestPipelineConfig {
targetFiles: string[];
testFramework: 'vitest' | 'jest';
coverageThreshold: number;
maxRetries: number;
}
interface PipelineResult {
generated: number;
validated: number;
passed: number;
failed: number;
coverageDelta: number;
}
async function runTestPipeline(
config: TestPipelineConfig
): Promise<PipelineResult> {
const results: PipelineResult = {
generated: 0,
validated: 0,
passed: 0,
failed: 0,
coverageDelta: 0,
};
for (const file of config.targetFiles) {
// Phase 1: テスト生成エージェント
const generatedTests = await generateTestAgent(file, {
framework: config.testFramework,
includeEdgeCases: true,
contextDepth: 3, // 呼び出し元を3階層まで追跡
});
results.generated += generatedTests.length;
// Phase 2: テスト検証エージェント
const validatedTests = await validateTestAgent(generatedTests, {
checkAssertionStrength: true,
checkMockAccuracy: true,
checkBoundaryConditions: true,
});
results.validated += validatedTests.length;
// Phase 3: テスト実行&フィードバックループ
const executionResult = await executeTestAgent(validatedTests, {
retries: config.maxRetries,
fixOnFailure: true,
});
results.passed += executionResult.passed;
results.failed += executionResult.failed;
}
return results;
}ポイントは contextDepth: 3 の部分です。テスト対象のファイルだけでなく、そのファイルを呼び出している上位3階層のコードもコンテキストとしてエージェントに渡します。これにより、「この関数がどういう文脈で使われているか」を AI が理解した上でテストを生成できます。
テスト生成エージェントの実装
まず、テストを生成するエージェントの具体的な実装です。Antigravity の agents.md を活用して、エージェントの振る舞いを細かく制御します。
// agents/test-pipeline/generator.ts
// テスト生成エージェント:ソースコードを解析し、テストケースを自動生成する
import { readFileSync, existsSync } from 'fs';
import { resolve, dirname } from 'path';
interface TestCase {
name: string;
type: 'unit' | 'integration' | 'e2e';
code: string;
targetFunction: string;
assertionCount: number;
}
interface GeneratorOptions {
framework: 'vitest' | 'jest';
includeEdgeCases: boolean;
contextDepth: number;
}
async function generateTestAgent(
filePath: string,
options: GeneratorOptions
): Promise<TestCase[]> {
// ソースコードの読み込みと依存関係の解析
const sourceCode = readFileSync(filePath, 'utf-8');
const imports = extractImports(sourceCode);
const functions = extractExportedFunctions(sourceCode);
// コンテキスト収集:呼び出し元を再帰的に辿る
const context = await collectContext(filePath, options.contextDepth);
const testCases: TestCase[] = [];
for (const fn of functions) {
// 正常系テスト
const happyPath = await generateHappyPathTest(fn, context, options);
testCases.push(happyPath);
// 異常系テスト
const errorCases = await generateErrorCases(fn, context, options);
testCases.push(...errorCases);
// エッジケース(オプション)
if (options.includeEdgeCases) {
const edgeCases = await generateEdgeCases(fn, context, options);
testCases.push(...edgeCases);
}
}
return testCases;
}
// 呼び出し元を再帰的に辿ってコンテキストを収集する
async function collectContext(
filePath: string,
depth: number,
visited: Set<string> = new Set()
): Promise<string[]> {
if (depth <= 0 || visited.has(filePath)) return [];
visited.add(filePath);
const contextParts: string[] = [];
// このファイルをインポートしている他のファイルを検索
const importers = await findImporters(filePath);
for (const importer of importers) {
const importerCode = readFileSync(importer, 'utf-8');
contextParts.push(
`// Context from: ${importer}\n${importerCode}`
);
// さらに上位の呼び出し元も辿る
const upperContext = await collectContext(
importer,
depth - 1,
visited
);
contextParts.push(...upperContext);
}
return contextParts;
}
function extractImports(code: string): string[] {
const importRegex = /import\s+.*?\s+from\s+['"](.+?)['"]/g;
const matches: string[] = [];
let match: RegExpExecArray | null;
while ((match = importRegex.exec(code)) \!== null) {
matches.push(match[1]);
}
return matches;
}
function extractExportedFunctions(code: string): string[] {
const fnRegex = /export\s+(?:async\s+)?function\s+(\w+)/g;
const matches: string[] = [];
let match: RegExpExecArray | null;
while ((match = fnRegex.exec(code)) \!== null) {
matches.push(match[1]);
}
return matches;
}collectContext 関数が肝です。テスト対象のファイルを import している全ファイルを再帰的に辿り、「この関数はどこでどう使われているか」をエージェントに伝えます。これだけで、生成されるテストの的確さが段違いに変わります。
テスト検証エージェント:AI が書いたテストを AI が検証する
生成されたテストを鵜呑みにしない——これがパイプライン全体の信頼性を担保するカギです。検証エージェントは、以下の3つの観点でテストを評価します。
// agents/test-pipeline/validator.ts
// テスト検証エージェント:生成されたテストの品質を多角的に検証する
interface ValidationResult {
testCase: TestCase;
isValid: boolean;
score: number; // 0-100 のスコア
issues: ValidationIssue[];
}
interface ValidationIssue {
type: 'weak-assertion' | 'missing-mock' | 'no-boundary' | 'redundant';
severity: 'error' | 'warning';
message: string;
suggestion: string;
}
async function validateTestAgent(
testCases: TestCase[],
options: {
checkAssertionStrength: boolean;
checkMockAccuracy: boolean;
checkBoundaryConditions: boolean;
}
): Promise<TestCase[]> {
const validatedTests: TestCase[] = [];
for (const testCase of testCases) {
const issues: ValidationIssue[] = [];
// 1. アサーションの強度チェック
if (options.checkAssertionStrength) {
const assertionIssues = checkAssertionStrength(testCase);
issues.push(...assertionIssues);
}
// 2. モックの正確性チェック
if (options.checkMockAccuracy) {
const mockIssues = checkMockAccuracy(testCase);
issues.push(...mockIssues);
}
// 3. 境界値テストの存在チェック
if (options.checkBoundaryConditions) {
const boundaryIssues = checkBoundaryConditions(testCase);
issues.push(...boundaryIssues);
}
const errorCount = issues.filter(i => i.severity === 'error').length;
if (errorCount === 0) {
validatedTests.push(testCase);
} else {
// 問題があるテストは修正を試みる
const fixedTest = await attemptFix(testCase, issues);
if (fixedTest) {
validatedTests.push(fixedTest);
}
// 修正不可能なテストは破棄する(品質を妥協しない)
}
}
return validatedTests;
}
// アサーションが甘すぎないかチェックする
function checkAssertionStrength(testCase: TestCase): ValidationIssue[] {
const issues: ValidationIssue[] = [];
const code = testCase.code;
// toBeTruthy() / toBeFalsy() だけのテストは弱い
const weakAssertions = code.match(/\.toBeTruthy\(\)|\.toBeFalsy\(\)/g);
if (weakAssertions && weakAssertions.length > 0) {
issues.push({
type: 'weak-assertion',
severity: 'warning',
message: `toBeTruthy/toBeFalsy が ${weakAssertions.length} 箇所で使われています`,
suggestion: 'toEqual() や toMatchObject() でより具体的な値を検証してください',
});
}
// expect() の数がテストケースに対して少なすぎる
const expectCount = (code.match(/expect\(/g) || []).length;
if (expectCount < 2) {
issues.push({
type: 'weak-assertion',
severity: 'error',
message: `アサーションが ${expectCount} 個しかありません`,
suggestion: '戻り値だけでなく、副作用(DB書き込み・API呼び出し等)も検証してください',
});
}
return issues;
}
// モックが本物の振る舞いと乖離していないかチェック
function checkMockAccuracy(testCase: TestCase): ValidationIssue[] {
const issues: ValidationIssue[] = [];
const code = testCase.code;
// vi.fn() で空のモックを返しているケース
const emptyMocks = code.match(/vi\.fn\(\)/g);
if (emptyMocks && emptyMocks.length > 2) {
issues.push({
type: 'missing-mock',
severity: 'warning',
message: `空の vi.fn() が ${emptyMocks.length} 箇所あります`,
suggestion: 'mockResolvedValue() や mockImplementation() で実際のレスポンス形状を再現してください',
});
}
return issues;
}このバリデーターが見つける典型的な問題を紹介します。
よくある間違い①:toBeTruthy() の多用
AI は「テストが通ること」を優先するため、アサーションを甘くしがちです。expect(result).toBeTruthy() は、result が 0 でも "" でも通ってしまいます。バリデーターはこれを検出し、toEqual() や toMatchObject() への置き換えを提案します。
よくある間違い②:空モックの量産
vi.fn() だけで型チェックを通過させるモックは、実際の API レスポンスと構造が異なることがあります。テストはグリーンでも、本番では想定外のプロパティが欠落してクラッシュする——という事故に直結します。
よくある間違い③:正常系しかテストしない
AI は「動くコード」のテストは得意ですが、「壊れるコード」のテストは苦手です。ネットワークエラー、タイムアウト、不正な入力値——こうしたシナリオを意図的に追加しないと、本番での障害対応力が上がりません。
テスト実行エージェント:失敗からの自動修復ループ
3層目のエージェントは、検証済みテストを実際に実行し、失敗したテストを自動修復します。
// agents/test-pipeline/executor.ts
// テスト実行エージェント:実行・失敗分析・自動修復のループ
import { exec } from 'child_process';
import { promisify } from 'util';
const execAsync = promisify(exec);
interface ExecutionResult {
passed: number;
failed: number;
fixedOnRetry: number;
unfixable: string[];
}
async function executeTestAgent(
testCases: TestCase[],
options: {
retries: number;
fixOnFailure: boolean;
}
): Promise<ExecutionResult> {
const result: ExecutionResult = {
passed: 0,
failed: 0,
fixedOnRetry: 0,
unfixable: [],
};
for (const testCase of testCases) {
let attempts = 0;
let currentTest = testCase;
let success = false;
while (attempts < options.retries && \!success) {
attempts++;
try {
// テストファイルを一時的に書き出して実行
const tempPath = writeTempTestFile(currentTest);
const { stdout, stderr } = await execAsync(
`npx vitest run ${tempPath} --reporter=json`,
{ timeout: 30000 }
);
const testResult = JSON.parse(stdout);
if (testResult.numFailedTests === 0) {
success = true;
result.passed++;
if (attempts > 1) {
result.fixedOnRetry++;
}
} else if (options.fixOnFailure) {
// 失敗原因を解析して修復を試みる
const failureReason = extractFailureReason(testResult);
currentTest = await repairTest(currentTest, failureReason);
}
} catch (error) {
// タイムアウトや実行エラー
if (attempts >= options.retries) {
result.failed++;
result.unfixable.push(currentTest.name);
}
}
}
if (\!success) {
result.failed++;
}
}
return result;
}
// 失敗原因を構造化して返す
function extractFailureReason(
testResult: Record<string, unknown>
): FailureReason {
// Vitest の JSON レポートから失敗情報を抽出
const failures = (testResult as any).testResults
?.flatMap((r: any) => r.assertionResults)
?.filter((a: any) => a.status === 'failed') ?? [];
if (failures.length === 0) {
return { type: 'unknown', message: 'No assertion failures found' };
}
const firstFailure = failures[0];
const message = firstFailure.failureMessages?.[0] ?? '';
// 失敗パターンの分類
if (message.includes('TypeError')) {
return { type: 'type-error', message, hint: 'モックの型が実装と不一致の可能性' };
}
if (message.includes('toEqual')) {
return { type: 'value-mismatch', message, hint: '期待値と実際の値が不一致' };
}
if (message.includes('timeout')) {
return { type: 'timeout', message, hint: '非同期処理のタイムアウト。await 漏れの可能性' };
}
return { type: 'assertion-failed', message, hint: 'アサーション条件を確認' };
}
interface FailureReason {
type: string;
message: string;
hint?: string;
}自動修復ループの効果は想像以上です。私のプロジェクトでは、初回生成で失敗したテストの約 60% が2回目のリトライで修復されました。特に多いのは、非同期処理の await 漏れと、モックの戻り値の型不一致です。
CI/CD パイプラインへの組み込み
ここまでのエージェントを GitHub Actions に組み込みます。プルリクエストごとに自動でテストが生成・検証・実行される仕組みです。
# .github/workflows/ai-test-pipeline.yml
# AI テストパイプライン:PR ごとにテストを自動生成・実行
name: AI Test Pipeline
on:
pull_request:
types: [opened, synchronize]
paths:
- 'src/**/*.ts'
- 'src/**/*.tsx'
jobs:
ai-test-generation:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # 差分比較のためフル履歴を取得
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
# 変更されたファイルを特定
- name: Detect changed files
id: changes
run: |
CHANGED=$(git diff --name-only origin/main...HEAD -- 'src/**/*.ts' 'src/**/*.tsx' | grep -v '\.test\.' | grep -v '\.spec\.')
echo "files<<EOF" >> $GITHUB_OUTPUT
echo "$CHANGED" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
# AI テスト生成パイプラインを実行
- name: Run AI Test Pipeline
run: |
npx tsx scripts/run-test-pipeline.ts \
--files "${{ steps.changes.outputs.files }}" \
--framework vitest \
--coverage-threshold 80 \
--max-retries 3
env:
ANTIGRAVITY_API_KEY: ${{ secrets.ANTIGRAVITY_API_KEY }}
# 生成されたテストをコミット
- name: Commit generated tests
run: |
git config user.name "AI Test Bot"
git config user.email "ai-test@example.com"
git add 'src/**/*.test.ts'
git diff --cached --quiet || git commit -m "test: AI generated tests for changed files"
git push
# カバレッジレポート
- name: Run coverage check
run: npx vitest run --coverage --reporter=json// scripts/run-test-pipeline.ts
// GitHub Actions から呼び出されるエントリポイント
import { parseArgs } from 'util';
const { values } = parseArgs({
options: {
files: { type: 'string' },
framework: { type: 'string', default: 'vitest' },
'coverage-threshold': { type: 'string', default: '80' },
'max-retries': { type: 'string', default: '3' },
},
});
async function main() {
const files = values.files?.split('\n').filter(Boolean) ?? [];
if (files.length === 0) {
console.log('✅ テスト対象の変更ファイルがありません');
process.exit(0);
}
console.log(`🧪 ${files.length} ファイルに対してテストパイプラインを実行`);
const result = await runTestPipeline({
targetFiles: files,
testFramework: (values.framework as 'vitest' | 'jest') ?? 'vitest',
coverageThreshold: parseInt(values['coverage-threshold'] ?? '80', 10),
maxRetries: parseInt(values['max-retries'] ?? '3', 10),
});
console.log('\n📊 パイプライン結果:');
console.log(` 生成: ${result.generated} テスト`);
console.log(` 検証通過: ${result.validated} テスト`);
console.log(` 実行成功: ${result.passed} テスト`);
console.log(` 実行失敗: ${result.failed} テスト`);
if (result.failed > 0) {
console.error('❌ 一部のテストが修復不能でした');
process.exit(1);
}
}
main().catch((error) => {
console.error('パイプラインエラー:', error);
process.exit(1);
});ここで1つ注意点があります。fetch-depth: 0 を指定してフル履歴を取得している理由は、git diff で変更ファイルの差分を正確に検出するためです。shallow clone だと差分が取れず、全ファイルが対象になってしまいます。
Antigravity の agents.md でエージェントの振る舞いを制御する
3つのエージェントの品質を安定させるために、agents.md でルールを明示的に定義します。
<\!-- .antigravity/agents.md -->
# テストパイプラインエージェント設定
## テスト生成エージェント
- テスト対象の関数だけでなく、呼び出し元のコンテキストも必ず参照すること
- 正常系・異常系・エッジケースの3パターンを必ず含めること
- expect() は1テストケースあたり最低2つ以上
- モックを使う場合、実際のレスポンス型と一致する値を設定すること
- テスト名は「何をテストしているか」が日本語で分かるように命名すること
## テスト検証エージェント
- toBeTruthy() / toBeFalsy() 単体でのアサーションは警告を出すこと
- 空のモック(vi.fn() のみ)が3つ以上ある場合は警告を出すこと
- 非同期関数のテストに await がない場合はエラーとすること
- テストが他のテストに依存している場合はエラーとすること
## テスト実行エージェント
- 最大リトライ回数は3回
- TypeError は「モック型不一致」として自動修復を試みること
- タイムアウトエラーは await 漏れを最初に疑うこと
- 3回リトライしても失敗するテストは破棄し、理由をログに記録することこの設定が効く場面は多々あります。たとえば「テスト名は日本語で」というルールを入れることで、テストが失敗したときに何が壊れたかがひと目で分かります。should return correct value ではなく ユーザー認証:期限切れトークンでリフレッシュが発動する と書かれていれば、ログを読む速度が格段に上がります。
E2E テスト生成の拡張:Playwright との連携
単体テストだけでなく、E2E テストもパイプラインに組み込みましょう。Antigravity のブラウザエージェントと Playwright を組み合わせます。
// agents/test-pipeline/e2e-generator.ts
// E2E テスト生成エージェント:ユーザーフローからテストを自動構築
interface UserFlow {
name: string;
steps: FlowStep[];
criticalPath: boolean; // 決済・認証などのクリティカルパス
}
interface FlowStep {
action: 'navigate' | 'click' | 'type' | 'wait' | 'assert';
target?: string; // CSS セレクタまたは aria ラベル
value?: string;
timeout?: number;
}
function generatePlaywrightTest(flow: UserFlow): string {
const steps = flow.steps.map(step => {
switch (step.action) {
case 'navigate':
return ` await page.goto('${step.value}');`;
case 'click':
return ` await page.getByRole('button', { name: '${step.target}' }).click();`;
case 'type':
return ` await page.getByLabel('${step.target}').fill('${step.value}');`;
case 'wait':
return ` await page.waitForResponse(resp => resp.url().includes('${step.value}'));`;
case 'assert':
return ` await expect(page.getByText('${step.value}')).toBeVisible();`;
default:
return ` // Unknown action: ${step.action}`;
}
}).join('\n');
return `
import { test, expect } from '@playwright/test';
test('${flow.name}', async ({ page }) => {
${steps}
});
`.trim();
}
// 使用例:ログインフローの E2E テスト
const loginFlow: UserFlow = {
name: 'ユーザーログイン:メールアドレスとパスワードで認証',
steps: [
{ action: 'navigate', value: '/login' },
{ action: 'type', target: 'メールアドレス', value: 'test@example.com' },
{ action: 'type', target: 'パスワード', value: 'SecurePassword123\!' },
{ action: 'click', target: 'ログイン' },
{ action: 'wait', value: '/api/auth' },
{ action: 'assert', value: 'ダッシュボード' },
],
criticalPath: true,
};
console.log(generatePlaywrightTest(loginFlow));
// 期待する出力:
// import { test, expect } from '@playwright/test';
//
// test('ユーザーログイン:メールアドレスとパスワードで認証', async ({ page }) => {
// await page.goto('/login');
// await page.getByLabel('メールアドレス').fill('test@example.com');
// ...
// });E2E テスト生成では、getByRole や getByLabel を優先的に使うように設計しています。CSS セレクタに依存すると、UI のリファクタリングでテストが壊れやすくなるためです。Playwright の推奨プラクティスに沿ったテストを AI に生成させることで、メンテナンスコストを大幅に削減できます。
カバレッジの可視化とフィードバック
テストパイプラインの効果を継続的に測定するために、カバレッジの推移を可視化します。
// scripts/coverage-tracker.ts
// カバレッジの推移を記録・可視化するユーティリティ
import { readFileSync, writeFileSync, existsSync } from 'fs';
interface CoverageEntry {
date: string;
commit: string;
statements: number;
branches: number;
functions: number;
lines: number;
aiGenerated: number; // AI 生成テスト数
manuallyWritten: number; // 手動テスト数
}
const HISTORY_FILE = '.coverage-history.json';
function recordCoverage(entry: CoverageEntry): void {
const history: CoverageEntry[] = existsSync(HISTORY_FILE)
? JSON.parse(readFileSync(HISTORY_FILE, 'utf-8'))
: [];
history.push(entry);
// 直近90日分のみ保持
const cutoff = new Date();
cutoff.setDate(cutoff.getDate() - 90);
const trimmed = history.filter(
e => new Date(e.date) >= cutoff
);
writeFileSync(HISTORY_FILE, JSON.stringify(trimmed, null, 2));
}
function generateReport(history: CoverageEntry[]): string {
if (history.length < 2) return '履歴が不足しています';
const latest = history[history.length - 1];
const previous = history[history.length - 2];
const delta = latest.lines - previous.lines;
const direction = delta >= 0 ? '↑' : '↓';
return [
`## カバレッジレポート (${latest.date})`,
'',
`| 指標 | 値 | 前回比 |`,
`|------|-----|--------|`,
`| Statements | ${latest.statements}% | ${direction} |`,
`| Branches | ${latest.branches}% | ${direction} |`,
`| Functions | ${latest.functions}% | ${direction} |`,
`| Lines | ${latest.lines}% | ${delta >= 0 ? '+' : ''}${delta}% |`,
'',
`AI生成テスト: ${latest.aiGenerated} / 手動テスト: ${latest.manuallyWritten}`,
`AI比率: ${Math.round(latest.aiGenerated / (latest.aiGenerated + latest.manuallyWritten) * 100)}%`,
].join('\n');
}私のプロジェクトでは、3ヶ月間でカバレッジが 30% → 85% に上昇し、そのうち AI 生成テストが全体の 72% を占めています。手動で書いているのは、ビジネスロジックの複雑な条件分岐に関するテストだけです。
よくある落とし穴と対処法
このパイプラインを運用する中で出会った落とし穴を3つ以上共有します。
落とし穴①:AI がテストを「通すために」コードを変更しようとする
Antigravity のエージェントは賢すぎるがゆえに、テストが通らないとソースコード側を修正しようとすることがあります。agents.md に「テスト修復時にソースコードを変更してはならない」と明記することで防げます。
落とし穴②:モックが多すぎてテストの意味がなくなる
依存関係が多い関数のテストでは、AI がすべての依存をモックしがちです。結果として「モックが正しく動くかのテスト」になり、本来の関数の振る舞いは検証されていないという事態が起きます。対策として、バリデーターに「モック数が5個を超えたら統合テストに格上げする」ルールを追加しました。
落とし穴③:非同期テストの race condition
AI は await を正しく挿入するものの、複数の非同期処理が並行する場合の race condition までは考慮しません。特に Promise.all を使ったテストで、テスト実行のたびに結果が変わる「フレーキーテスト」が生成されることがあります。vi.useFakeTimers() を条件付きで注入するロジックを実行エージェントに追加して対処しました。
落とし穴④:テスト名の重複
大量にテストを生成すると、テスト名が重複して Vitest が警告を出します。生成エージェント側で「関数名 + シナリオ + 連番」のフォーマットを強制することで解消しました。
パイプラインの段階的な導入ロードマップ
いきなりフルパイプラインを導入する必要はありません。段階的に始めることをおすすめします。
Week 1-2:テスト生成エージェントだけを導入する
まずは手動でエージェントを動かし、生成されたテストを自分の目で確認します。この段階では CI に組み込まず、品質のベースラインを把握します。
Week 3-4:検証エージェントを追加する
生成されたテストの品質に慣れてきたら、バリデーターを追加します。ここで「甘いアサーション」や「空モック」がどれくらいあるかを定量的に把握できます。
Week 5-8:CI/CD に組み込む
検証エージェントの精度に満足できたら、GitHub Actions に組み込みます。最初は「テスト生成のみ(実行はしない)」モードで導入し、安定したら自動実行を有効にします。
この段階的アプローチを取ることで、「AI が生成したテストがいつの間にか CI を壊した」という事故を防げます。
次のステップ
まずは、プロジェクト内で最もテストが不足しているファイルを1つ選び、テスト生成エージェントを手動で動かしてみてください。agents.md にルールを1つだけ追加するだけでも、生成品質の違いを実感できるはずです。
テスト自動化は、最初の一歩が最も大変で、そこを超えると加速度的に楽になります。AI は「テストを書く」のが得意なのではなく、「テストを書くための判断基準を学習する」のが得意です。良いルールを与えれば、それに応じた品質のテストを返してくれます。
Antigravity のエージェント機能を使ったテスト自動化のさらに発展的なパターン(ミューテーションテスト、プロパティベーステスト等)については、AI テスト生成の基本やTDD 実践マスタリーも合わせてご覧ください。
テスト設計の体系的な