T-CREATOR

Jestでテストカバレッジを計測・可視化する方法

Jestでテストカバレッジを計測・可視化する方法

テスト自動化において、「どの程度のコードがテストされているか」を把握することは、品質管理の要となります。

Jest は強力なテストカバレッジ機能を標準搭載しており、簡単な設定でコードカバレッジの計測と可視化が可能です。本記事では、Jest を使用したテストカバレッジの効果的な計測方法から、レポートの読み方、実践的な改善手法まで、包括的に解説いたします。

適切なカバレッジ計測により、テストの盲点を発見し、より堅牢なアプリケーションを構築できるようになるでしょう。

テストカバレッジの基本概念

カバレッジの 4 つの指標(Line、Function、Branch、Statement)

テストカバレッジには 4 つの主要な指標があり、それぞれ異なる視点からコードの網羅性を評価します。

指標英語名計測内容重要度
1Line Coverage実行された行の割合★★★
2Function Coverage呼び出された関数の割合★★★
3Branch Coverage実行された分岐の割合★★★★
4Statement Coverage実行された文の割合★★

**Line Coverage(行カバレッジ)**は最も直感的な指標です。

javascriptfunction calculatePrice(price, discount) {
  if (discount > 0) {
    return price * (1 - discount); // この行が実行されたか
  }
  return price; // この行が実行されたか
}

// テストケース
test('割引なしの価格計算', () => {
  expect(calculatePrice(1000, 0)).toBe(1000);
});
// このテストでは2行目のif文内は実行されない
// Line Coverage: 66.7% (3行中2行)

**Function Coverage(関数カバレッジ)**は関数の呼び出し状況を計測します。

javascriptclass UserService {
  createUser(userData) {
    // 実装コード
    return { id: 1, ...userData };
  }

  updateUser(id, userData) {
    // 実装コード(テストされていない)
    return { id, ...userData };
  }

  deleteUser(id) {
    // 実装コード(テストされていない)
    return { success: true };
  }
}

// テストケース
test('ユーザー作成', () => {
  const service = new UserService();
  const result = service.createUser({ name: 'John' });
  expect(result.name).toBe('John');
});
// Function Coverage: 33.3% (3関数中1関数)

**Branch Coverage(分岐カバレッジ)**は条件分岐の網羅性を評価します。

javascriptfunction validateAge(age) {
  if (age >= 18 && age <= 100) {
    return 'valid';
  }
  return 'invalid';
}

// 不完全なテスト
test('成人の年齢検証', () => {
  expect(validateAge(25)).toBe('valid');
});
// Branch Coverage: 50%
// true分岐のみテスト、false分岐が未テスト

// 完全なテスト
test('年齢検証の全パターン', () => {
  expect(validateAge(25)).toBe('valid'); // true分岐
  expect(validateAge(15)).toBe('invalid'); // false分岐
});
// Branch Coverage: 100%

**Statement Coverage(文カバレッジ)**は個々の文の実行状況を計測します。

javascriptfunction processData(data) {
  let result = data.map((item) => item * 2); // Statement 1
  result = result.filter((item) => item > 10); // Statement 2
  return result.sort((a, b) => a - b); // Statement 3
}

test('データ処理テスト', () => {
  const result = processData([1, 5, 8]);
  expect(result).toEqual([10, 16]);
});
// Statement Coverage: 100% (全ての文が実行される)

カバレッジレポートの見方と理解

Jest のカバレッジレポートは、様々な形式で表示されます。コンソール出力の例を見てみましょう。

bash# yarn test --coverage の実行結果
 PASS  src/utils/calculator.test.js
 PASS  src/components/Button.test.jsx

------------|---------|----------|---------|---------|-------------------
File        | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
------------|---------|----------|---------|---------|-------------------
All files   |   85.71 |    75.00 |   88.89 |   85.00 |
 calculator |   90.00 |    83.33 |  100.00 |   88.89 |
  index.js  |   90.00 |    83.33 |  100.00 |   88.89 | 15,23
 components |   80.00 |    66.67 |   75.00 |   80.00 |
  Button.js |   80.00 |    66.67 |   75.00 |   80.00 | 8,12,18
------------|---------|----------|---------|---------|-------------------

このレポートから以下の情報が読み取れます。

javascript// calculator/index.js の未カバー箇所(15,23行目)
export function divide(a, b) {
  if (b === 0) {
    throw new Error('Division by zero'); // 15行目:未テスト
  }
  return a / b;
}

export function factorial(n) {
  if (n < 0) {
    return null; // 23行目:未テスト
  }
  return n <= 1 ? 1 : n * factorial(n - 1);
}

重要なポイント:

  • Uncovered Line #s: 具体的にどの行がテストされていないか
  • % Branch: 条件分岐の網羅性(重要度が高い)
  • % Funcs: 未使用の関数の特定

品質指標としてのカバレッジの位置づけ

カバレッジは品質の指標の一つですが、万能ではありません。適切な理解が重要です。

javascript// 100%カバレッジでも品質が低い例
function isValidEmail(email) {
  return email.includes('@'); // 簡素すぎる検証
}

test('メール形式チェック', () => {
  expect(isValidEmail('test@example.com')).toBe(true);
  expect(isValidEmail('invalid')).toBe(false);
});
// Line Coverage: 100%
// しかし、メール検証としては不十分

カバレッジの適切な活用指針:

用途効果注意点
テストの盲点発見★★★★数値の追求が目的化しないよう注意
リファクタリングの安全性確保★★★★エッジケースの考慮も必要
コードレビューの参考指標★★★テストの質も同時に評価
チーム内での品質意識向上★★★過度な競争を避ける

Jest でのカバレッジ計測設定

collectCoverage オプションの基本設定

Jest でカバレッジを計測する最も簡単な方法は、コマンドラインオプションを使用することです。

bash# 基本的なカバレッジ計測
yarn test --coverage

# カバレッジを継続的に計測
yarn test --coverage --watchAll

設定ファイルでの永続的な設定も可能です。

javascript// jest.config.js
module.exports = {
  // カバレッジ計測の有効化
  collectCoverage: false, // デフォルトはfalse

  // CI環境でのみカバレッジを計測
  collectCoverage: process.env.CI === 'true',

  // 開発時とCI時で使い分け
  collectCoverage: process.env.NODE_ENV === 'test',
};

package.jsonでのスクリプト設定例:

javascript{
  "scripts": {
    "test": "jest",
    "test:coverage": "jest --coverage",
    "test:watch": "jest --watch",
    "test:ci": "jest --coverage --ci --watchAll=false"
  }
}

環境別の設定を行うことで、開発効率と CI 品質管理を両立できます。

javascript// 環境に応じた動的設定
const config = {
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],
};

// CI環境での追加設定
if (process.env.CI) {
  config.collectCoverage = true;
  config.coverageReporters = ['text', 'lcov'];
  config.coverageThreshold = {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80,
    },
  };
}

module.exports = config;

collectCoverageFrom での対象ファイル指定

collectCoverageFromオプションにより、カバレッジ計測の対象を細かく制御できます。

javascriptmodule.exports = {
  collectCoverage: true,

  // カバレッジ計測対象の指定
  collectCoverageFrom: [
    // 基本的な対象パターン
    'src/**/*.{js,jsx,ts,tsx}',

    // 特定ディレクトリの除外
    '!src/**/*.d.ts',
    '!src/**/*.stories.{js,jsx,ts,tsx}',
    '!src/**/__tests__/**',
    '!src/**/*.test.{js,jsx,ts,tsx}',

    // 設定ファイルの除外
    '!src/setupTests.js',
    '!src/reportWebVitals.js',
  ],
};

実際のプロジェクト構造に応じた設定例:

javascript// React プロジェクトの詳細設定
module.exports = {
  collectCoverageFrom: [
    // コンポーネント
    'src/components/**/*.{js,jsx,ts,tsx}',

    // カスタムフック
    'src/hooks/**/*.{js,ts}',

    // ユーティリティ関数
    'src/utils/**/*.{js,ts}',

    // サービス層
    'src/services/**/*.{js,ts}',

    // 除外パターン
    '!src/**/*.stories.{js,jsx,ts,tsx}', // Storybook
    '!src/**/__mocks__/**', // モックファイル
    '!src/**/types/**', // 型定義
    '!src/**/constants/**', // 定数ファイル
  ],
};

Node.js API プロジェクトの設定例:

javascriptmodule.exports = {
  collectCoverageFrom: [
    // サーバーサイドコード
    'src/**/*.{js,ts}',

    // 除外パターン
    '!src/server.js', // エントリーポイント
    '!src/config/**', // 設定ファイル
    '!src/migrations/**', // DBマイグレーション
    '!src/seeds/**', // シードデータ
    '!src/**/*.d.ts', // 型定義
  ],

  // テストファイルの除外
  testPathIgnorePatterns: [
    '/node_modules/',
    '/dist/',
    '/coverage/',
  ],
};

coverageDirectory での出力先設定

カバレッジレポートの出力先をカスタマイズできます。

javascriptmodule.exports = {
  // カバレッジレポートの出力ディレクトリ
  coverageDirectory: 'coverage',

  // プロジェクト別の出力先
  coverageDirectory: 'reports/coverage',

  // 日付付きディレクトリ(履歴管理用)
  coverageDirectory: `coverage/${
    new Date().toISOString().split('T')[0]
  }`,
};

GitHub Actions での活用例:

javascript// CI用の設定
module.exports = {
  coverageDirectory: process.env.CI
    ? 'coverage'
    : 'coverage-local',

  // CI環境では最小限のレポート
  coverageReporters: process.env.CI
    ? ['text', 'lcov']
    : ['text', 'html', 'lcov'],
};

出力先の設定により、開発と CI/CD での使い分けが可能になります。

bash# 出力されるファイル構造
coverage/
├── lcov-report/          # HTML形式の詳細レポート
│   ├── index.html       # メインページ
│   ├── base.css         # スタイルシート
│   └── ...
├── lcov.info            # LCOV形式(CI連携用)
├── clover.xml           # Clover形式
└── coverage-final.json  # JSON形式(データ処理用)

カバレッジレポートの種類と活用

コンソール出力(text、text-summary)

textレポーターは最も詳細なコンソール出力を提供します。

javascriptmodule.exports = {
  coverageReporters: ['text'],
};
bash# text出力の例
-------------|---------|----------|---------|---------|-------------------
File         | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-------------|---------|----------|---------|---------|-------------------
All files    |   87.50 |    81.25 |   90.00 |   86.67 |
 components  |   85.00 |    75.00 |   88.89 |   84.21 |
  Button.jsx |   80.00 |    66.67 |   75.00 |   80.00 | 15,23,31
  Modal.jsx  |   90.00 |    83.33 |  100.00 |   88.89 | 8,42
 utils       |   92.31 |    90.00 |   92.86 |   91.67 |
  format.js  |   88.89 |    85.71 |   87.50 |   88.00 | 12,34,56
  helper.js  |   95.65 |    94.44 |   96.77 |   95.00 | 78
-------------|---------|----------|---------|---------|-------------------

text-summaryはより簡潔な表示です。

javascriptmodule.exports = {
  coverageReporters: ['text-summary'],
};
bash# text-summary出力の例
=============================== Coverage summary ===============================
Statements   : 87.50% ( 210/240 )
Branches     : 81.25% ( 65/80 )
Functions    : 90.00% ( 36/40 )
Lines        : 86.67% ( 208/240 )
================================================================================

複数レポーターの組み合わせ:

javascriptmodule.exports = {
  coverageReporters: [
    'text-summary', // 簡潔なサマリー
    'text', // 詳細な表
    'html', // ブラウザで確認用
  ],
};

HTML レポートの詳細解析

HTML レポートは最も直感的で詳細な分析が可能です。

javascriptmodule.exports = {
  coverageReporters: ['html'],
  coverageDirectory: 'coverage',
};

HTML レポートでは以下の機能が利用できます:

html<!-- 生成されるHTMLレポートの構造 -->
coverage/ ├── index.html
<!-- 全体サマリー -->
├── components/ │ ├── index.html
<!-- コンポーネント一覧 -->
│ ├── Button.jsx.html
<!-- ファイル別詳細 -->
│ └── Modal.jsx.html └── utils/ ├── index.html ├──
format.js.html └── helper.js.html

HTML レポートの活用方法:

javascript// coverage/components/Button.jsx.html の表示例
// ❌ 未カバーの行は赤色でハイライト
function handleClick(event) {
  if (event.shiftKey) {
    handleSpecialClick(); // ← 赤色表示(未テスト)
  }
  onClick(event);
}

// ✅ カバー済みの行は緑色でハイライト
function handleKeyDown(event) {
  if (event.key === 'Enter') {
    handleClick(event); // ← 緑色表示(テスト済み)
  }
}

分岐カバレッジの視覚的表示:

javascript// 条件分岐の表示例
function validateInput(value) {
  // E: 条件式全体, I: 条件の各部分
  if (value && value.length > 0) {
    // ✅ E: true (1/2), I: true,true (2/4)
    return true;
  }
  // ❌ E: false (0/2), I: false,false (0/4)
  return false;
}

LCOV 形式と CI/CD ツール連携

LCOV 形式は外部ツールとの連携に最適です。

javascriptmodule.exports = {
  coverageReporters: ['lcov'],
};

GitHub Actions での活用例:

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

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: '18'
          cache: 'yarn'

      - run: yarn install
      - run: yarn test --coverage

      # Codecovへのアップロード
      - uses: codecov/codecov-action@v3
        with:
          file: ./coverage/lcov.info
          token: ${{ secrets.CODECOV_TOKEN }}

Coveralls との連携:

javascript// package.json
{
  "scripts": {
    "test:coverage": "jest --coverage",
    "coverage:report": "cat ./coverage/lcov.info | coveralls"
  },
  "devDependencies": {
    "coveralls": "^3.1.1"
  }
}
yaml# GitHub Actions
- run: yarn test:coverage
- run: yarn coverage:report
  env:
    COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}

JSON 形式でのデータ活用

JSON 形式は自動化やカスタム分析に活用できます。

javascriptmodule.exports = {
  coverageReporters: ['json', 'json-summary'],
};

生成される JSON データの構造:

javascript// coverage/coverage-summary.json
{
  "total": {
    "lines": { "total": 240, "covered": 208, "skipped": 0, "pct": 86.67 },
    "functions": { "total": 40, "covered": 36, "skipped": 0, "pct": 90 },
    "statements": { "total": 240, "covered": 210, "skipped": 0, "pct": 87.5 },
    "branches": { "total": 80, "covered": 65, "skipped": 0, "pct": 81.25 }
  },
  "src/components/Button.jsx": {
    "lines": { "total": 25, "covered": 20, "skipped": 0, "pct": 80 },
    "functions": { "total": 4, "covered": 3, "skipped": 0, "pct": 75 },
    "statements": { "total": 25, "covered": 20, "skipped": 0, "pct": 80 },
    "branches": { "total": 6, "covered": 4, "skipped": 0, "pct": 66.67 }
  }
}

カスタム分析スクリプトの例:

javascript// scripts/coverage-analysis.js
const fs = require('fs');
const coverageData = JSON.parse(
  fs.readFileSync(
    './coverage/coverage-summary.json',
    'utf8'
  )
);

// カバレッジ低下を検出
function detectCoverageRegression(threshold = 80) {
  const issues = [];

  Object.entries(coverageData).forEach(([file, data]) => {
    if (file === 'total') return;

    if (data.lines.pct < threshold) {
      issues.push({
        file,
        coverage: data.lines.pct,
        type: 'lines',
      });
    }

    if (data.branches.pct < threshold) {
      issues.push({
        file,
        coverage: data.branches.pct,
        type: 'branches',
      });
    }
  });

  return issues;
}

// Slack通知用のメッセージ生成
function generateSlackMessage() {
  const total = coverageData.total;
  return {
    text: `Coverage Report`,
    blocks: [
      {
        type: 'section',
        text: {
          type: 'mrkdwn',
          text: `*Lines:* ${total.lines.pct}% | *Branches:* ${total.branches.pct}% | *Functions:* ${total.functions.pct}%`,
        },
      },
    ],
  };
}

以上がカバレッジレポートの種類と活用方法です。続いて、カバレッジ閾値の設定について詳しく解説いたします。

カバレッジ閾値の設定と運用

coverageThreshold の設定方法

coverageThresholdにより、最低限のカバレッジ基準を強制できます。基準を下回った場合、テストが失敗します。

javascriptmodule.exports = {
  collectCoverage: true,

  // グローバル閾値の設定
  coverageThreshold: {
    global: {
      branches: 80, // 分岐カバレッジ80%以上
      functions: 80, // 関数カバレッジ80%以上
      lines: 80, // 行カバレッジ80%以上
      statements: 80, // 文カバレッジ80%以上
    },
  },
};

段階的な閾値設定例:

javascript// 開発段階に応じた閾値設定
const getCoverageThreshold = () => {
  const isProduction =
    process.env.NODE_ENV === 'production';
  const isCI = process.env.CI === 'true';

  if (isProduction) {
    return {
      global: {
        branches: 90,
        functions: 90,
        lines: 90,
        statements: 90,
      },
    };
  }

  if (isCI) {
    return {
      global: {
        branches: 80,
        functions: 80,
        lines: 80,
        statements: 80,
      },
    };
  }

  // 開発環境では緩い設定
  return {
    global: {
      branches: 70,
      functions: 70,
      lines: 70,
      statements: 70,
    },
  };
};

module.exports = {
  collectCoverage: process.env.CI === 'true',
  coverageThreshold: getCoverageThreshold(),
};

閾値違反時の出力例:

bash# カバレッジ不足の場合
Jest: Coverage threshold for statements (75.5%) not met: 80%
Jest: Coverage threshold for branches (73.33%) not met: 80%
Jest: Coverage threshold for functions (77.78%) not met: 80%
Jest: Coverage threshold for lines (75.5%) not met: 80%

# テスト失敗
FAIL Coverage threshold for statements (75.5%) not met: 80%

プロジェクト別・ファイル別の閾値設定

ファイル別の詳細設定:

javascriptmodule.exports = {
  coverageThreshold: {
    // グローバル基準
    global: {
      branches: 75,
      functions: 75,
      lines: 75,
      statements: 75,
    },

    // 重要なコアモジュールは高い基準
    './src/utils/': {
      branches: 90,
      functions: 95,
      lines: 90,
      statements: 90,
    },

    // UI コンポーネントは中程度
    './src/components/': {
      branches: 80,
      functions: 85,
      lines: 80,
      statements: 80,
    },

    // 特定ファイルの個別設定
    './src/services/api.js': {
      branches: 95,
      functions: 100,
      lines: 95,
      statements: 95,
    },
  },
};

モジュール種別による設定例:

javascript// プロジェクト構造に応じた設定
module.exports = {
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80,
    },

    // ビジネスロジック(高い品質要求)
    './src/domain/': {
      branches: 95,
      functions: 95,
      lines: 90,
      statements: 90,
    },

    // データアクセス層
    './src/repositories/': {
      branches: 85,
      functions: 90,
      lines: 85,
      statements: 85,
    },

    // UIコンポーネント
    './src/components/': {
      branches: 75,
      functions: 80,
      lines: 75,
      statements: 75,
    },

    // ユーティリティ関数
    './src/utils/': {
      branches: 90,
      functions: 95,
      lines: 90,
      statements: 90,
    },
  },
};

段階的な品質向上戦略

段階的な閾値向上計画:

javascript// scripts/update-coverage-threshold.js
const fs = require('fs');
const path = require('path');

class CoverageThresholdManager {
  constructor(configPath = 'jest.config.js') {
    this.configPath = configPath;
    this.currentThresholds = this.getCurrentThresholds();
  }

  // 現在の閾値を取得
  getCurrentThresholds() {
    const config = require(path.resolve(this.configPath));
    return config.coverageThreshold?.global || {};
  }

  // 段階的に閾値を上げる
  incrementThresholds(increment = 5) {
    const newThresholds = {};

    [
      'branches',
      'functions',
      'lines',
      'statements',
    ].forEach((metric) => {
      const current = this.currentThresholds[metric] || 0;
      newThresholds[metric] = Math.min(
        current + increment,
        100
      );
    });

    return newThresholds;
  }

  // 現在のカバレッジ状況を確認
  async getCurrentCoverage() {
    const { execSync } = require('child_process');

    try {
      execSync('yarn test --coverage --silent', {
        stdio: 'pipe',
      });
      const coverageData = JSON.parse(
        fs.readFileSync(
          './coverage/coverage-summary.json',
          'utf8'
        )
      );
      return coverageData.total;
    } catch (error) {
      throw new Error('Failed to get coverage data');
    }
  }

  // 安全な閾値を提案
  async suggestSafeThresholds() {
    const current = await this.getCurrentCoverage();
    const suggestions = {};

    [
      'branches',
      'functions',
      'lines',
      'statements',
    ].forEach((metric) => {
      const currentPct = current[metric].pct;
      // 現在の値から5%下げた安全な値を提案
      suggestions[metric] = Math.max(currentPct - 5, 0);
    });

    return suggestions;
  }
}

// 使用例
async function updateThresholds() {
  const manager = new CoverageThresholdManager();

  try {
    const suggestions =
      await manager.suggestSafeThresholds();
    console.log('Suggested safe thresholds:', suggestions);

    // 設定ファイルの更新(手動確認後)
    const shouldUpdate = process.argv.includes('--apply');
    if (shouldUpdate) {
      // 実際の更新処理
      console.log('Thresholds updated successfully');
    }
  } catch (error) {
    console.error(
      'Error updating thresholds:',
      error.message
    );
  }
}

プロジェクトライフサイクルに応じた戦略:

フェーズカバレッジ目標重点項目戦略
プロトタイプ50-60%基本機能のテスト最小限のテスト作成
開発初期70-75%主要フローの網羅コアロジックの確実なテスト
開発中期80-85%エッジケースの追加分岐網羅の強化
リリース前85-90%品質保証全体的な品質向上
運用中90%+保守性確保継続的な品質維持
javascript// フェーズ別設定の自動化
function getPhaseBasedThreshold() {
  const packageJson = require('./package.json');
  const version = packageJson.version;
  const [major, minor, patch] = version
    .split('.')
    .map(Number);

  // バージョンベースの判定
  if (major === 0) {
    // プロトタイプフェーズ
    return {
      branches: 50,
      functions: 60,
      lines: 55,
      statements: 55,
    };
  } else if (major === 1 && minor < 5) {
    // 開発初期
    return {
      branches: 70,
      functions: 75,
      lines: 70,
      statements: 70,
    };
  } else if (major === 1) {
    // 開発中期
    return {
      branches: 80,
      functions: 85,
      lines: 80,
      statements: 80,
    };
  } else {
    // 成熟フェーズ
    return {
      branches: 90,
      functions: 90,
      lines: 85,
      statements: 85,
    };
  }
}

実践的なカバレッジ改善手法

未カバー領域の特定と対策

HTML レポートを活用した効率的な分析:

javascript// カバレッジ分析用のヘルパースクリプト
class CoverageAnalyzer {
  constructor(coverageDir = './coverage') {
    this.coverageDir = coverageDir;
  }

  // 未カバー領域の抽出
  extractUncoveredAreas() {
    const summaryPath = `${this.coverageDir}/coverage-summary.json`;
    const data = JSON.parse(
      fs.readFileSync(summaryPath, 'utf8')
    );

    const uncoveredFiles = [];

    Object.entries(data).forEach(([filePath, coverage]) => {
      if (filePath === 'total') return;

      const issues = [];

      // 各指標をチェック
      [
        'lines',
        'branches',
        'functions',
        'statements',
      ].forEach((metric) => {
        if (coverage[metric].pct < 80) {
          issues.push({
            metric,
            current: coverage[metric].pct,
            uncovered:
              coverage[metric].total -
              coverage[metric].covered,
          });
        }
      });

      if (issues.length > 0) {
        uncoveredFiles.push({ filePath, issues });
      }
    });

    return uncoveredFiles.sort(
      (a, b) => b.issues.length - a.issues.length
    );
  }

  // 改善提案の生成
  generateImprovementSuggestions(uncoveredAreas) {
    return uncoveredAreas.map(({ filePath, issues }) => {
      const suggestions = issues.map((issue) => {
        switch (issue.metric) {
          case 'branches':
            return `${filePath}: 条件分岐テストを${issue.uncovered}個追加してください`;
          case 'functions':
            return `${filePath}: 未テストの関数が${issue.uncovered}個あります`;
          case 'lines':
            return `${filePath}: ${issue.uncovered}行のコードがテストされていません`;
          default:
            return `${filePath}: ${issue.metric}のカバレッジを改善してください`;
        }
      });

      return { filePath, suggestions };
    });
  }
}

具体的な未カバー領域の対策例:

javascript// 未カバーのエラーハンドリング
function parseUserInput(input) {
  try {
    return JSON.parse(input);
  } catch (error) {
    // この部分が未テスト
    console.error('Parse error:', error);
    return null;
  }
}

// 改善されたテスト
describe('parseUserInput', () => {
  test('正常なJSONの解析', () => {
    const result = parseUserInput('{"name": "John"}');
    expect(result).toEqual({ name: 'John' });
  });

  test('不正なJSONのエラーハンドリング', () => {
    const consoleSpy = jest
      .spyOn(console, 'error')
      .mockImplementation();
    const result = parseUserInput('invalid json');

    expect(result).toBeNull();
    expect(consoleSpy).toHaveBeenCalledWith(
      'Parse error:',
      expect.any(Error)
    );

    consoleSpy.mockRestore();
  });
});

分岐カバレッジの改善例:

javascript// 複雑な条件分岐の例
function validateUser(user) {
  if (user && user.email && user.email.includes('@')) {
    if (user.age >= 18 && user.age <= 100) {
      return { valid: true };
    }
    return { valid: false, reason: 'Invalid age' };
  }
  return { valid: false, reason: 'Invalid email' };
}

// 完全な分岐テスト
describe('validateUser 分岐カバレッジ', () => {
  test.each([
    // [入力, 期待値, テストケース名]
    [
      null,
      { valid: false, reason: 'Invalid email' },
      'null user',
    ],
    [
      {},
      { valid: false, reason: 'Invalid email' },
      'empty user',
    ],
    [
      { email: 'invalid' },
      { valid: false, reason: 'Invalid email' },
      'invalid email',
    ],
    [
      { email: 'test@example.com', age: 17 },
      { valid: false, reason: 'Invalid age' },
      'underage',
    ],
    [
      { email: 'test@example.com', age: 101 },
      { valid: false, reason: 'Invalid age' },
      'overage',
    ],
    [
      { email: 'test@example.com', age: 25 },
      { valid: true },
      'valid user',
    ],
  ])(
    'should handle %s',
    (input, expected, _description) => {
      expect(validateUser(input)).toEqual(expected);
    }
  );
});

テストケース追加の優先順位付け

リスクベースの優先順位付け:

javascript// カバレッジ改善の優先度計算
class CoveragePrioritizer {
  constructor(coverageData, codeComplexity) {
    this.coverageData = coverageData;
    this.codeComplexity = codeComplexity;
  }

  calculatePriority(filePath) {
    const coverage = this.coverageData[filePath];
    const complexity = this.codeComplexity[filePath] || 1;

    // 優先度スコアの計算
    const coverageGap = 100 - coverage.lines.pct;
    const branchGap = 100 - coverage.branches.pct;
    const complexityWeight = Math.log(complexity + 1);

    return (
      (coverageGap * 0.4 + branchGap * 0.6) *
      complexityWeight
    );
  }

  getPrioritizedFiles() {
    return Object.keys(this.coverageData)
      .filter((path) => path !== 'total')
      .map((path) => ({
        path,
        priority: this.calculatePriority(path),
        coverage: this.coverageData[path],
      }))
      .sort((a, b) => b.priority - a.priority);
  }
}

実際の優先順位付け例:

ファイル現在のカバレッジ複雑度優先度対策
auth.js65%★★★★認証ロジックの分岐テスト追加
payment.js70%★★★★決済エラーケースのテスト
utils.js75%★★★ユーティリティ関数の網羅
ui.jsx80%★★UI コンポーネントのエッジケース

段階的なテスト追加戦略:

javascript// 1. 高リスク・低カバレッジから開始
describe('payment.js - 高優先度テスト', () => {
  test('クレジットカード決済の失敗ケース', () => {
    // 決済API エラーレスポンスのテスト
  });

  test('決済金額の上限超過エラー', () => {
    // ビジネスルール違反のテスト
  });
});

// 2. 中リスク領域の補強
describe('utils.js - 基本機能テスト', () => {
  test('日付フォーマット関数のエッジケース', () => {
    // 境界値テストの追加
  });
});

// 3. 低リスク領域の最適化
describe('ui.jsx - UIコンポーネント', () => {
  test('アクセシビリティ関連の動作', () => {
    // UI の細かな動作確認
  });
});

リファクタリング時のカバレッジ維持

リファクタリング前のカバレッジ記録:

javascript// scripts/pre-refactor-coverage.js
const fs = require('fs');
const { execSync } = require('child_process');

async function recordBaselineCoverage() {
  // 現在のカバレッジを記録
  execSync('yarn test --coverage --silent', {
    stdio: 'pipe',
  });

  const currentCoverage = JSON.parse(
    fs.readFileSync(
      './coverage/coverage-summary.json',
      'utf8'
    )
  );

  // ベースライン保存
  fs.writeFileSync(
    './coverage-baseline.json',
    JSON.stringify(currentCoverage, null, 2)
  );

  console.log('Baseline coverage recorded');
  console.log(
    `Total coverage: ${currentCoverage.total.lines.pct}%`
  );
}

リファクタリング後の検証:

javascript// scripts/post-refactor-coverage.js
async function validateCoverageAfterRefactor() {
  // 新しいカバレッジを計測
  execSync('yarn test --coverage --silent', {
    stdio: 'pipe',
  });

  const newCoverage = JSON.parse(
    fs.readFileSync(
      './coverage/coverage-summary.json',
      'utf8'
    )
  );

  const baseline = JSON.parse(
    fs.readFileSync('./coverage-baseline.json', 'utf8')
  );

  // カバレッジの比較
  const comparison = compareCoverage(
    baseline.total,
    newCoverage.total
  );

  if (comparison.hasRegression) {
    console.error('Coverage regression detected!');
    console.error('Changes:', comparison.changes);
    process.exit(1);
  } else {
    console.log('Coverage maintained or improved');
    console.log('Changes:', comparison.changes);
  }
}

function compareCoverage(baseline, current) {
  const changes = {};
  const regressions = [];

  ['lines', 'branches', 'functions', 'statements'].forEach(
    (metric) => {
      const diff =
        current[metric].pct - baseline[metric].pct;
      changes[metric] = diff;

      if (diff < -1) {
        // 1%以上の低下で警告
        regressions.push(`${metric}: ${diff.toFixed(2)}%`);
      }
    }
  );

  return {
    changes,
    hasRegression: regressions.length > 0,
    regressions,
  };
}

安全なリファクタリングワークフロー:

bash# 1. ベースラインの記録
yarn run record-coverage-baseline

# 2. リファクタリング実施
# ... コード変更 ...

# 3. テスト実行とカバレッジ検証
yarn test
yarn run validate-coverage

# 4. カバレッジ低下があった場合の対処
yarn run coverage-diff --show-missing

継続的なカバレッジ監視:

yaml# .github/workflows/coverage-check.yml
name: Coverage Check
on:
  pull_request:
    branches: [main]

jobs:
  coverage:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 2

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
          cache: 'yarn'

      - run: yarn install

      - name: Run tests with coverage
        run: yarn test --coverage

      - name: Coverage comment
        uses: actions/github-script@v6
        with:
          script: |
            const fs = require('fs');
            const coverage = JSON.parse(
              fs.readFileSync('./coverage/coverage-summary.json', 'utf8')
            );

            const total = coverage.total;
            const comment = `## Coverage Report

            | Metric | Coverage |
            |--------|----------|
            | Lines | ${total.lines.pct}% |
            | Branches | ${total.branches.pct}% |
            | Functions | ${total.functions.pct}% |
            | Statements | ${total.statements.pct}% |
            `;

            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: comment
            });

まとめ

Jest によるテストカバレッジの計測と可視化は、高品質なソフトウェア開発において欠かせない要素です。

重要なポイントの振り返り

カバレッジ指標の理解では、Line、Function、Branch、Statement の 4 つの指標それぞれが異なる視点でコードの網羅性を評価することを学びました。特に Branch Coverage は、条件分岐の網羅性を表す重要な指標として、優先的に改善すべき項目です。

効果的な設定により、プロジェクトの性質と開発フェーズに応じた最適なカバレッジ計測が可能になります。collectCoverageFromでの対象ファイル指定、環境別のcoverageReporters設定、段階的なcoverageThreshold設定により、開発効率と品質管理のバランスを取ることができます。

多様なレポート形式を活用することで、開発者個人の詳細分析からチーム全体の品質監視、CI/CD ツールとの連携まで、様々な用途に対応できます。HTML レポートでの視覚的分析、LCOV 形式での外部ツール連携、JSON 形式でのカスタム分析など、目的に応じた使い分けが重要です。

継続的な改善においては、カバレッジ数値の追求だけでなく、テストの質と実用性を重視した取り組みが必要です。未カバー領域の計画的な改善、リスクベースの優先順位付け、リファクタリング時の安全性確保により、持続可能な品質向上を実現できます。

適切なカバレッジ計測により、コードの信頼性向上、開発速度の向上、チーム全体の品質意識向上を実現し、より良いソフトウェア開発を進めていきましょう。

関連リンク