- Gemini CLIでカバレッジレポートを分析し、未テスト箇所のテストを自動生成できる
- GitHub Actions / Cloud Buildとの統合でカバレッジ品質ゲートを自動化
- SES案件で求められるテスト自動化スキルの習得にGemini CLIが最適
「テストカバレッジが30%しかないからまず70%まで上げてほしい」「CIパイプラインにカバレッジチェックを組み込みたい」——SES案件で品質改善を任されたエンジニアが最も頻繁に直面する課題の一つが、テストカバレッジの向上です。
しかし現実には、大量の未テストコードに対してテストを一つずつ書いていくのは膨大な時間がかかります。どこから手をつけるべきか、どのテストパターンを使うべきか、判断にも時間がかかります。
この記事では、Gemini CLIを活用してテストカバレッジの分析から改善、CI統合までを効率的に自動化する方法を、実践的なコード例とともに詳しく解説します。
- テストカバレッジの種類と各指標の意味
- Gemini CLIでカバレッジレポートを分析し、優先度の高い未テスト箇所を特定する方法
- 未テストコードに対するテストの自動生成テクニック
- GitHub Actions / Cloud Buildでの品質ゲート構築
- SES現場でのテスト戦略とキャリアへの影響
テストカバレッジの基礎知識
カバレッジの種類と意味
テストカバレッジには複数の種類があり、それぞれ異なる観点からコードの網羅性を測定します。
| カバレッジ種別 | 測定対象 | 推奨値 | 重要度 |
|---|---|---|---|
| 行カバレッジ | 実行された行の割合 | 80%以上 | ★★★★☆ |
| 分岐カバレッジ | 条件分岐の網羅率 | 70%以上 | ★★★★★ |
| 関数カバレッジ | 呼び出された関数の割合 | 90%以上 | ★★★☆☆ |
| 文カバレッジ | 実行された文の割合 | 80%以上 | ★★★★☆ |
なぜカバレッジ80%が目安なのか
# カバレッジと品質の相関
100% ┤ ←────── ここを目指すとコスパが激減
90% ┤ ▓▓▓▓▓▓▓▓▓▓
80% ┤ ▓▓▓▓▓▓▓▓▓▓▓▓ ← 推奨ライン(コスパ最大)
70% ┤ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓
60% ┤ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
└──────────────────→ テスト作成工数
80%カバレッジは、重要なビジネスロジックとエラーハンドリングをカバーしつつ、テスト作成の工数を合理的な範囲に収める最適なバランスポイントとして、業界で広く採用されています。
Gemini CLIでカバレッジレポートを分析する
カバレッジレポートの生成と解析
まず、既存のテストを実行してカバレッジレポートを生成します。
# Jest(TypeScript)の場合
npx jest --coverage --coverageReporters=json-summary --coverageReporters=lcov
# Pytestの場合
pytest --cov=src --cov-report=json --cov-report=html
次に、Gemini CLIでカバレッジレポートを分析させます:
gemini "coverage/coverage-summary.jsonとsrc/以下のソースコードを分析して、
以下を教えて:
1. カバレッジが最も低いファイルTOP10
2. 各ファイルの未テスト箇所(行番号と内容)
3. テスト追加の優先度(ビジネスクリティカル度 × カバレッジの低さ)
4. 推定工数(各ファイルのテスト追加にかかる時間)"
未テスト箇所の自動特定
gemini "src/services/payment-service.tsのカバレッジレポートを見て、
テストされていない分岐とエラーハンドリングを特定して。
各未テスト箇所について:
- 何をテストすべきか
- テストの重要度(高/中/低)
- 推奨テストパターン(正常系/異常系/境界値)
を一覧にして。"
Gemini CLIの分析結果例:
## 未テスト箇所の分析結果
### payment-service.ts
| 行番号 | 未テスト内容 | 重要度 | 推奨テストパターン |
|-------|------------|--------|----------------|
| L45-52 | クレジットカード認証失敗時のリトライロジック | 高 | 異常系: 429/503エラー時の指数バックオフ |
| L78-85 | 二重課金防止のべき等性チェック | 高 | 境界値: 同一決済IDでの重複リクエスト |
| L112-120 | 金額がマイナスの場合のバリデーション | 中 | 境界値: 0, -1, MAX_SAFE_INTEGER |
| L145-160 | タイムアウト発生時のロールバック処理 | 高 | 異常系: タイムアウト後の状態整合性 |
| L178-185 | 監査ログの出力 | 低 | 正常系: ログフォーマットの検証 |
Gemini CLIでテストを自動生成する
関数単位でのテスト自動生成
gemini "src/services/payment-service.tsのprocessPayment関数に対して、
以下のテストケースを生成して:
- 正常系: 有効なクレジットカードでの決済成功
- 異常系: カード認証失敗(3回リトライ後に失敗)
- 異常系: 二重課金防止のべき等性テスト
- 境界値: 金額が0、マイナス、上限値のケース
- 異常系: タイムアウト時のロールバック
Jestのdescribe/it形式で、モック設計も含めて。"
// src/services/__tests__/payment-service.test.ts
import { PaymentService } from '../payment-service';
import { PaymentGateway } from '../../infrastructure/payment-gateway';
import { AuditLogger } from '../../infrastructure/audit-logger';
// モックの設定
jest.mock('../../infrastructure/payment-gateway');
jest.mock('../../infrastructure/audit-logger');
describe('PaymentService', () => {
let service: PaymentService;
let mockGateway: jest.Mocked<PaymentGateway>;
let mockLogger: jest.Mocked<AuditLogger>;
beforeEach(() => {
mockGateway = new PaymentGateway() as jest.Mocked<PaymentGateway>;
mockLogger = new AuditLogger() as jest.Mocked<AuditLogger>;
service = new PaymentService(mockGateway, mockLogger);
jest.clearAllMocks();
});
describe('processPayment - 正常系', () => {
it('有効なクレジットカードで決済が成功する', async () => {
mockGateway.charge.mockResolvedValue({
transactionId: 'tx_123',
status: 'succeeded',
});
const result = await service.processPayment({
orderId: 'order_1',
amount: 5000,
currency: 'JPY',
cardToken: 'tok_valid',
});
expect(result.status).toBe('succeeded');
expect(result.transactionId).toBe('tx_123');
expect(mockGateway.charge).toHaveBeenCalledTimes(1);
expect(mockLogger.log).toHaveBeenCalledWith(
expect.objectContaining({ action: 'payment_processed' })
);
});
});
describe('processPayment - リトライロジック', () => {
it('一時的なエラーで3回リトライ後に成功する', async () => {
mockGateway.charge
.mockRejectedValueOnce(new Error('429 Too Many Requests'))
.mockRejectedValueOnce(new Error('503 Service Unavailable'))
.mockResolvedValue({
transactionId: 'tx_456',
status: 'succeeded',
});
const result = await service.processPayment({
orderId: 'order_2',
amount: 3000,
currency: 'JPY',
cardToken: 'tok_valid',
});
expect(result.status).toBe('succeeded');
expect(mockGateway.charge).toHaveBeenCalledTimes(3);
});
it('3回リトライしても失敗する場合はエラーを投げる', async () => {
mockGateway.charge.mockRejectedValue(
new Error('503 Service Unavailable')
);
await expect(
service.processPayment({
orderId: 'order_3',
amount: 3000,
currency: 'JPY',
cardToken: 'tok_valid',
})
).rejects.toThrow('Payment processing failed after 3 retries');
});
});
describe('processPayment - べき等性', () => {
it('同一注文IDでの重複リクエストは二重課金しない', async () => {
mockGateway.charge.mockResolvedValue({
transactionId: 'tx_789',
status: 'succeeded',
});
// 1回目
await service.processPayment({
orderId: 'order_dup',
amount: 5000,
currency: 'JPY',
cardToken: 'tok_valid',
});
// 2回目(同一注文ID)
const result = await service.processPayment({
orderId: 'order_dup',
amount: 5000,
currency: 'JPY',
cardToken: 'tok_valid',
});
expect(result.status).toBe('already_processed');
expect(mockGateway.charge).toHaveBeenCalledTimes(1);
});
});
describe('processPayment - 境界値', () => {
it.each([
{ amount: 0, desc: '金額が0の場合' },
{ amount: -1, desc: '金額がマイナスの場合' },
{ amount: -100, desc: '金額が大きなマイナスの場合' },
])('$desc はバリデーションエラーを返す', async ({ amount }) => {
await expect(
service.processPayment({
orderId: 'order_boundary',
amount,
currency: 'JPY',
cardToken: 'tok_valid',
})
).rejects.toThrow('Invalid amount');
});
it('金額が上限値(999,999,999)を超える場合はエラー', async () => {
await expect(
service.processPayment({
orderId: 'order_max',
amount: 1_000_000_000,
currency: 'JPY',
cardToken: 'tok_valid',
})
).rejects.toThrow('Amount exceeds maximum');
});
});
describe('processPayment - タイムアウト', () => {
it('タイムアウト発生時にロールバックされる', async () => {
mockGateway.charge.mockImplementation(
() => new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), 100)
)
);
await expect(
service.processPayment({
orderId: 'order_timeout',
amount: 5000,
currency: 'JPY',
cardToken: 'tok_valid',
})
).rejects.toThrow('Payment timeout');
expect(mockGateway.refund).toHaveBeenCalled();
});
});
});
CI/CDパイプラインにカバレッジゲートを組み込む
GitHub Actionsでのカバレッジチェック
gemini "GitHub Actionsのワークフローにカバレッジチェックを追加して。
要件:
- PRごとにカバレッジを測定
- カバレッジ80%未満ならPRをブロック
- カバレッジの変化をPRコメントに自動投稿
- カバレッジバッジの自動更新"
# .github/workflows/coverage-gate.yml
name: Coverage Gate
on:
pull_request:
branches: [main, develop]
permissions:
contents: read
pull-requests: write
jobs:
coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
- run: npm ci
- name: Run tests with coverage
run: npx jest --coverage --coverageReporters=json-summary --coverageReporters=text
- name: Check coverage threshold
run: |
COVERAGE=$(node -e "
const report = require('./coverage/coverage-summary.json');
const total = report.total;
const avg = (
total.lines.pct +
total.branches.pct +
total.functions.pct +
total.statements.pct
) / 4;
console.log(avg.toFixed(2));
")
echo "Average coverage: ${COVERAGE}%"
if (( $(echo "$COVERAGE < 80" | bc -l) )); then
echo "::error::Coverage ${COVERAGE}% is below 80% threshold"
exit 1
fi
- name: Post coverage comment
if: always()
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const report = JSON.parse(
fs.readFileSync('coverage/coverage-summary.json', 'utf8')
);
const total = report.total;
const body = `## 📊 テストカバレッジレポート
| 種別 | カバレッジ | 閾値 | 結果 |
|------|----------|------|------|
| 行 | ${total.lines.pct}% | 80% | ${total.lines.pct >= 80 ? '✅' : '❌'} |
| 分岐 | ${total.branches.pct}% | 70% | ${total.branches.pct >= 70 ? '✅' : '❌'} |
| 関数 | ${total.functions.pct}% | 90% | ${total.functions.pct >= 90 ? '✅' : '❌'} |
| 文 | ${total.statements.pct}% | 80% | ${total.statements.pct >= 80 ? '✅' : '❌'} |
`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body,
});
Google Cloud Buildでのカバレッジ統合
# cloudbuild.yaml
steps:
- name: 'node:22'
entrypoint: 'bash'
args:
- '-c'
- |
npm ci
npx jest --coverage --coverageReporters=json-summary
COVERAGE=$(node -e "
const r = require('./coverage/coverage-summary.json');
console.log(r.total.lines.pct);
")
echo "Line coverage: ${COVERAGE}%"
if [ $(echo "$COVERAGE < 80" | bc) -eq 1 ]; then
echo "Coverage below threshold!" && exit 1
fi
Gemini CLIによるカバレッジ改善のワークフロー
ステップバイステップの改善プロセス
# Step 1: 現在のカバレッジを確認
npx jest --coverage
# Step 2: Gemini CLIで改善計画を立てる
gemini "coverage/coverage-summary.jsonを分析して、
カバレッジを現在の45%から80%に上げるための
改善計画を優先度付きで作成して。
各ファイルの推定テスト追加工数も含めて。"
# Step 3: 優先度の高いファイルからテストを生成
gemini "src/services/order-service.tsの未テスト箇所に対する
テストを全て生成して。カバレッジ100%を目標に。"
# Step 4: 生成されたテストを実行して確認
npx jest src/services/__tests__/order-service.test.ts --coverage
# Step 5: 次のファイルへ
gemini "次に優先度の高いsrc/services/inventory-service.tsの
テストを生成して。"
ミューテーションテストとの連携
テストカバレッジが高くても、テストの品質が低ければ意味がありません。ミューテーションテストは、コードに意図的な変更(ミューテーション)を加え、テストがそれを検出できるかを検証します。
gemini "Stryker(ミューテーションテスト)の設定を作成して。
src/services/配下のファイルを対象に。
結果レポートの解釈方法も教えて。"

テスト戦略のベストプラクティス
テストピラミッドに基づくカバレッジ配分
/\
/ \ E2Eテスト(10%)
/ \ → 主要ユーザーフロー
/------\
/ \ 統合テスト(30%)
/ \ → API・DB連携
/------------\
/ \ ユニットテスト(60%)
/ \ → ビジネスロジック
/──────────────────\
CLAUDE.mdでのテスト規約定義
# Testing Rules for Gemini CLI
## カバレッジ目標
- 行カバレッジ: 80%以上
- 分岐カバレッジ: 70%以上
- 新規コードは100%を目指す
## テスト命名規則
- describe: テスト対象のクラス/関数名
- it: 「〜の場合、〜する」形式(日本語OK)
## テスト必須パターン
- 正常系: 最低1ケース
- 異常系: エラー種別ごとに1ケース
- 境界値: 0, 1, MAX, 空文字列, null
SES現場でのテストカバレッジ改善の実績
実践パターンと期待効果
| パターン | Before | After | 所要期間 | 単価への影響 |
|---|---|---|---|---|
| レガシーシステムのテスト追加 | 15% | 70% | 2ヶ月 | +10-15万/月 |
| 新規開発でのTDD導入 | 0% | 85% | 継続的 | +5-10万/月 |
| CI/CDパイプライン構築 | 手動 | 自動 | 2週間 | +10-20万/月 |
高単価案件で求められるテストスキル
必要スキル: Jest/Vitest + CI/CD + カバレッジ分析
想定単価: 70-95万円/月
案件例:
- 金融系システムのテスト自動化
- ECサイトのリグレッションテスト基盤構築
- マイクロサービスの統合テスト設計
まとめ
Gemini CLIを活用したテストカバレッジの分析・改善・CI統合について、実践的な手法を解説しました。
- テストカバレッジ80%は品質とコストのバランスが最も良い目標値
- Gemini CLIでカバレッジレポートを分析し、優先度の高い未テスト箇所を自動特定できる
- テストの自動生成により、カバレッジ改善の工数を大幅に削減できる
- GitHub Actions / Cloud Buildの品質ゲートで、カバレッジの自動チェックが可能
- SES案件でテスト自動化スキルは70〜95万円/月の案件に直結する
次のステップ: テストカバレッジの基礎を押さえたら、Gemini CLIでCI/CDパイプラインを最適化する方法で、より包括的な品質管理に挑戦しましょう。
関連記事: