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

テスト自動化において、「どの程度のコードがテストされているか」を把握することは、品質管理の要となります。
Jest は強力なテストカバレッジ機能を標準搭載しており、簡単な設定でコードカバレッジの計測と可視化が可能です。本記事では、Jest を使用したテストカバレッジの効果的な計測方法から、レポートの読み方、実践的な改善手法まで、包括的に解説いたします。
適切なカバレッジ計測により、テストの盲点を発見し、より堅牢なアプリケーションを構築できるようになるでしょう。
テストカバレッジの基本概念
カバレッジの 4 つの指標(Line、Function、Branch、Statement)
テストカバレッジには 4 つの主要な指標があり、それぞれ異なる視点からコードの網羅性を評価します。
指標 | 英語名 | 計測内容 | 重要度 |
---|---|---|---|
1 | Line Coverage | 実行された行の割合 | ★★★ |
2 | Function Coverage | 呼び出された関数の割合 | ★★★ |
3 | Branch Coverage | 実行された分岐の割合 | ★★★★ |
4 | Statement 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.js | 65% | 高 | ★★★★ | 認証ロジックの分岐テスト追加 |
payment.js | 70% | 高 | ★★★★ | 決済エラーケースのテスト |
utils.js | 75% | 中 | ★★★ | ユーティリティ関数の網羅 |
ui.jsx | 80% | 低 | ★★ | 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 形式でのカスタム分析など、目的に応じた使い分けが重要です。
継続的な改善においては、カバレッジ数値の追求だけでなく、テストの質と実用性を重視した取り組みが必要です。未カバー領域の計画的な改善、リスクベースの優先順位付け、リファクタリング時の安全性確保により、持続可能な品質向上を実現できます。
適切なカバレッジ計測により、コードの信頼性向上、開発速度の向上、チーム全体の品質意識向上を実現し、より良いソフトウェア開発を進めていきましょう。
関連リンク
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来