Day 8: カバレッジとデバッグ
今日学ぶこと
- コードカバレッジの概念(行、分岐、関数、ステートメント)
- カバレッジレポートの実行と読み方
- カバレッジしきい値の設定
- カバレッジが教えてくれること・教えてくれないこと
- デバッグテクニック(console.log、debugger、VS Code連携)
- Jestの便利なCLIオプションとコンフィグ
- よくあるJestエラーのトラブルシューティング
コードカバレッジとは
コードカバレッジは、テストがソースコードのどの部分を実行したかを計測する指標です。テストの品質を客観的に把握するための手段の一つです。
flowchart TB
subgraph Coverage["カバレッジの4つの指標"]
STMT["ステートメント\nStatements"]
BRANCH["分岐\nBranches"]
FUNC["関数\nFunctions"]
LINE["行\nLines"]
end
STMT -->|"実行された文の割合"| S1["全文のうち何%が\n実行されたか"]
BRANCH -->|"条件分岐の網羅率"| S2["if/elseの全パスを\n通ったか"]
FUNC -->|"呼び出された関数の割合"| S3["定義した関数が\n呼ばれたか"]
LINE -->|"実行された行の割合"| S4["何行が実行\nされたか"]
style Coverage fill:#3b82f6,color:#fff
4つのカバレッジ指標
| 指標 | 英語名 | 説明 | 例 |
|---|---|---|---|
| ステートメント | Statements | 実行された文の割合 | const x = 1; が実行されたか |
| 分岐 | Branches | if/else、三項演算子などの条件分岐の網羅率 | if (x > 0) のtrue/false両方を通ったか |
| 関数 | Functions | 呼び出された関数の割合 | 定義した関数が1回以上呼ばれたか |
| 行 | Lines | 実行された行の割合 | 各行が実行されたか |
カバレッジの具体例
以下のコードでカバレッジの計測を見てみましょう。
// mathUtils.js
function calculate(a, b, operation) { // Line 1: function
if (operation === 'add') { // Line 2: branch 1
return a + b; // Line 3
} else if (operation === 'subtract') {// Line 4: branch 2
return a - b; // Line 5
} else { // Line 6: branch 3
throw new Error('Unknown operation');// Line 7
}
}
module.exports = { calculate };
// mathUtils.test.js
const { calculate } = require('./mathUtils');
test('adds two numbers', () => {
expect(calculate(1, 2, 'add')).toBe(3);
});
このテストだけだと、カバレッジは以下のようになります。
| 指標 | 値 | 理由 |
|---|---|---|
| Statements | 62.5% | Line 5, 7 が未実行 |
| Branches | 33.3% | 3つの分岐のうち1つだけ通過 |
| Functions | 100% | calculate は呼ばれた |
| Lines | 62.5% | Line 5, 7 が未実行 |
カバレッジレポートの実行
--coverage フラグ
npx jest --coverage
実行すると、ターミナルにテーブル形式のレポートが表示されます。
----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 62.5 | 33.33 | 100 | 62.5 |
mathUtils.js | 62.5 | 33.33 | 100 | 62.5 | 5,7
----------|---------|----------|---------|---------|-------------------
HTMLレポート
カバレッジを実行すると、coverage/ ディレクトリにHTMLレポートが生成されます。
npx jest --coverage
open coverage/lcov-report/index.html
flowchart LR
subgraph Report["HTMLカバレッジレポート"]
INDEX["index.html\n全体サマリー"]
FILE["各ファイルページ\n行ごとの詳細"]
COLOR["色分け表示"]
end
INDEX --> FILE
FILE --> COLOR
COLOR --> GREEN["緑: 実行済み"]
COLOR --> RED["赤: 未実行"]
COLOR --> YELLOW["黄: 部分的に実行"]
style Report fill:#8b5cf6,color:#fff
style GREEN fill:#22c55e,color:#fff
style RED fill:#ef4444,color:#fff
style YELLOW fill:#f59e0b,color:#fff
HTMLレポートでは以下の情報が確認できます。
- 緑色: テストによって実行されたコード
- 赤色: テストによって実行されなかったコード
- 黄色: 部分的に実行されたコード(分岐の片方のみ通過)
- 行番号の横の
xN: その行が何回実行されたか
カバレッジしきい値の設定
jest.config.js でカバレッジの最低基準を設定できます。しきい値を下回るとテストが失敗します。
// jest.config.js
module.exports = {
collectCoverage: true,
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
};
TypeScript版(jest.config.ts):
// jest.config.ts
import type { Config } from 'jest';
const config: Config = {
collectCoverage: true,
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
};
export default config;
ファイル別のしきい値
特定のファイルやディレクトリに個別のしきい値を設定することもできます。
// jest.config.js
module.exports = {
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
'./src/utils/': {
branches: 90,
functions: 95,
lines: 90,
statements: 90,
},
},
};
カバレッジ収集対象の指定
// jest.config.js
module.exports = {
collectCoverageFrom: [
'src/**/*.{js,ts}',
'!src/**/*.d.ts', // type definition files
'!src/**/index.{js,ts}', // barrel files
'!src/**/*.stories.{js,ts}', // storybook files
],
};
カバレッジが教えてくれること・教えてくれないこと
flowchart TB
subgraph Good["カバレッジが教えてくれること"]
G1["どのコードが\nテストされていないか"]
G2["テストの\n網羅度の目安"]
G3["リグレッションの\nリスクが高い箇所"]
end
subgraph Bad["カバレッジが教えてくれないこと"]
B1["テストの品質\nアサーションの妥当性"]
B2["エッジケースの\nカバー状況"]
B3["テストが正しい\nビジネスロジックを\n検証しているか"]
end
style Good fill:#22c55e,color:#fff
style Bad fill:#ef4444,color:#fff
高カバレッジ ≠ 高品質
// bad example: 100% coverage but no real assertion
function multiply(a, b) {
return a * b;
}
test('multiply', () => {
multiply(2, 3);
// no expect() — test passes, coverage is 100%
// but we're not actually verifying anything!
});
// good example: meaningful assertion
test('multiply returns the product of two numbers', () => {
expect(multiply(2, 3)).toBe(6);
expect(multiply(0, 5)).toBe(0);
expect(multiply(-1, 3)).toBe(-3);
});
ベストプラクティス: カバレッジは「テストされていない箇所を見つけるツール」として使いましょう。カバレッジ100%を目標にするのではなく、重要なビジネスロジックの品質を確保することが大切です。
デバッグテクニック
1. console.log デバッグ
最もシンプルなデバッグ方法です。
test('debug with console.log', () => {
const data = { name: 'Alice', age: 25 };
console.log('data:', data); // simple output
console.log('type:', typeof data); // type check
console.log('keys:', Object.keys(data)); // structure
console.dir(data, { depth: null }); // deep object
expect(data.name).toBe('Alice');
});
Tip: テストが多い場合、
console.logの出力が他のテスト出力に埋もれることがあります。--verboseフラグと組み合わせると見つけやすくなります。
2. debugger と Node.js インスペクタ
node --inspect-brk node_modules/.bin/jest --runInBand
ブラウザで chrome://inspect を開き、Node.js のデバッグセッションに接続できます。
test('debug with debugger', () => {
const result = complexFunction();
debugger; // execution pauses here
expect(result).toBe(42);
});
3. VS Code での Jest デバッグ
.vscode/launch.json に以下の設定を追加します。
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Jest: Current File",
"program": "${workspaceFolder}/node_modules/.bin/jest",
"args": [
"${relativeFile}",
"--config",
"jest.config.js",
"--no-coverage"
],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen"
},
{
"type": "node",
"request": "launch",
"name": "Jest: All Tests",
"program": "${workspaceFolder}/node_modules/.bin/jest",
"args": ["--runInBand", "--no-coverage"],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen"
}
]
}
flowchart LR
subgraph Debug["デバッグ方法"]
LOG["console.log\nシンプル・手軽"]
DEBUGGER["debugger\nステップ実行"]
VSCODE["VS Code\nブレークポイント"]
end
LOG -->|"初心者向け"| Q1["素早い確認"]
DEBUGGER -->|"中級者向け"| Q2["詳細な調査"]
VSCODE -->|"上級者向け"| Q3["効率的な開発"]
style LOG fill:#22c55e,color:#fff
style DEBUGGER fill:#f59e0b,color:#fff
style VSCODE fill:#3b82f6,color:#fff
便利なCLIオプション
単一ファイルの実行
# run a specific test file
npx jest src/utils/math.test.js
# pattern matching
npx jest math
# run tests matching a name pattern
npx jest -t "adds two numbers"
--verbose フラグ
テスト結果を詳細に表示します。各テストの名前と結果が個別に表示されます。
npx jest --verbose
PASS src/math.test.js
calculate
✓ adds two numbers (3 ms)
✓ subtracts two numbers (1 ms)
✓ throws for unknown operation (2 ms)
--bail フラグ
最初のテスト失敗時に実行を中断します。CI環境での時間短縮に便利です。
# stop after first failure
npx jest --bail
# stop after N failures
npx jest --bail=3
--watch モード
ファイル変更を監視して自動再実行します。
npx jest --watch # changed files only
npx jest --watchAll # all tests
その他の便利なフラグ
| フラグ | 説明 |
|---|---|
--coverage |
カバレッジレポートを生成 |
--verbose |
詳細なテスト結果を表示 |
--bail |
最初の失敗で中断 |
--watch |
変更ファイルのテストを自動再実行 |
--watchAll |
全テストを自動再実行 |
--runInBand |
テストを直列実行(デバッグ時に便利) |
--no-cache |
キャッシュを無視して実行 |
--clearCache |
Jestのキャッシュをクリア |
--detectOpenHandles |
テスト終了を妨げるハンドルを検出 |
--forceExit |
テスト完了後に強制終了 |
jest.config.js の便利なオプション
// jest.config.js
module.exports = {
// test file patterns
testMatch: ['**/__tests__/**/*.{js,ts}', '**/*.test.{js,ts}'],
// regex pattern for test file paths
testPathPattern: 'src/utils',
// default timeout for each test (ms)
testTimeout: 10000,
// automatically clear mocks between tests
clearMocks: true,
// coverage settings
collectCoverage: false,
coverageDirectory: 'coverage',
coverageReporters: ['text', 'lcov', 'clover'],
// module path aliases
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
},
// setup files
setupFilesAfterSetup: ['<rootDir>/jest.setup.js'],
// ignore patterns
testPathIgnorePatterns: ['/node_modules/', '/dist/'],
coveragePathIgnorePatterns: ['/node_modules/', '/dist/'],
// transform settings for TypeScript
transform: {
'^.+\\.tsx?$': 'ts-jest',
},
};
TypeScript版:
// jest.config.ts
import type { Config } from 'jest';
const config: Config = {
testMatch: ['**/__tests__/**/*.{js,ts}', '**/*.test.{js,ts}'],
testTimeout: 10000,
clearMocks: true,
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
},
};
export default config;
| オプション | 説明 | デフォルト |
|---|---|---|
testMatch |
テストファイルのパターン | ['**/__tests__/**/*.[jt]s?(x)', '**/?(*.)+(spec|test).[jt]s?(x)'] |
testPathPattern |
テストパスの正規表現フィルタ | (なし) |
testTimeout |
各テストのタイムアウト(ms) | 5000 |
clearMocks |
テスト間でモックを自動クリア | false |
verbose |
詳細な結果を表示 | false |
bail |
失敗時に中断 | 0(中断なし) |
maxWorkers |
並列実行のワーカー数 | CPUコア数の半分 |
よくあるJestエラーのトラブルシューティング
1. "Cannot find module" エラー
Cannot find module './utils' from 'src/app.test.js'
原因と解決策:
// check file path and extension
// jest.config.js
module.exports = {
moduleFileExtensions: ['js', 'ts', 'json'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
},
};
2. "SyntaxError: Unexpected token" エラー
SyntaxError: Unexpected token 'export'
原因: ESM構文がトランスパイルされていない
// jest.config.js
module.exports = {
transform: {
'^.+\\.jsx?$': 'babel-jest',
'^.+\\.tsx?$': 'ts-jest',
},
transformIgnorePatterns: [
'/node_modules/(?!(some-esm-package)/)',
],
};
3. "Async callback was not invoked" エラー
Timeout - Async callback was not invoked within the 5000 ms timeout
原因: 非同期処理が完了していない
// increase timeout for slow tests
test('slow async operation', async () => {
const result = await slowOperation();
expect(result).toBeDefined();
}, 30000); // 30 second timeout
// or set globally
// jest.config.js
module.exports = {
testTimeout: 30000,
};
4. テストが終了しない
Jest did not exit one second after the test run has completed.
原因: 開いたままのハンドル(DB接続、タイマーなど)
# detect open handles
npx jest --detectOpenHandles
# force exit (last resort)
npx jest --forceExit
// proper cleanup
afterAll(async () => {
await db.close();
await server.close();
});
5. テスト間の状態汚染
// problem: tests share state
let counter = 0;
test('first', () => {
counter++;
expect(counter).toBe(1);
});
test('second', () => {
// counter is already 1!
expect(counter).toBe(0); // FAILS
});
// solution: reset state in beforeEach
let counter;
beforeEach(() => {
counter = 0;
});
test('first', () => {
counter++;
expect(counter).toBe(1);
});
test('second', () => {
expect(counter).toBe(0); // PASSES
});
トラブルシューティングチェックリスト
flowchart TB
START["テストが失敗した"] --> Q1{"エラーメッセージは\n何か?"}
Q1 -->|"Module not found"| A1["パスとmoduleNameMapper\nを確認"]
Q1 -->|"Syntax Error"| A2["transformと\nbabel設定を確認"]
Q1 -->|"Timeout"| A3["testTimeoutの\n延長を検討"]
Q1 -->|"Won't exit"| A4["--detectOpenHandles\nで原因特定"]
Q1 -->|"Assertion failed"| A5["console.logで\n値を確認"]
A1 --> FIX["修正して再実行"]
A2 --> FIX
A3 --> FIX
A4 --> FIX
A5 --> FIX
style START fill:#ef4444,color:#fff
style FIX fill:#22c55e,color:#fff
まとめ
| 概念 | 説明 |
|---|---|
| ステートメントカバレッジ | 実行された文の割合 |
| ブランチカバレッジ | 条件分岐の網羅率 |
| 関数カバレッジ | 呼び出された関数の割合 |
| 行カバレッジ | 実行された行の割合 |
--coverage |
カバレッジレポートを生成するCLIフラグ |
coverageThreshold |
カバレッジの最低基準を設定 |
--verbose |
テスト結果を詳細表示 |
--bail |
最初の失敗で中断 |
--runInBand |
テストを直列実行(デバッグ用) |
--detectOpenHandles |
未閉鎖ハンドルを検出 |
重要ポイント
- カバレッジは「テストされていない箇所を見つけるツール」であり、テストの品質指標ではない
- カバレッジしきい値を設定してCI/CDで品質ゲートとして活用する
- デバッグは
console.logから始めて、必要に応じてdebuggerやVS Code連携へ --bail、--verbose、--runInBandを組み合わせてデバッグ効率を上げる- よくあるエラーのパターンを覚えて、素早く対処する
練習問題
問題1: 基本
以下の関数のカバレッジを100%にするテストを書いてください。
function getGrade(score) {
if (score >= 90) return 'A';
if (score >= 80) return 'B';
if (score >= 70) return 'C';
if (score >= 60) return 'D';
return 'F';
}
問題2: 応用
以下の jest.config.js を完成させてください。条件は:
- カバレッジしきい値は全指標80%
src/配下のみカバレッジ収集.d.tsファイルは除外- テストのタイムアウトは10秒
// jest.config.js
module.exports = {
// your config here
};
チャレンジ問題
以下のコードのブランチカバレッジが50%になるテストケースと、100%になるテストケースをそれぞれ書いてください。
function processOrder(order) {
if (!order) {
throw new Error('Order is required');
}
let total = order.price * order.quantity;
if (order.coupon) {
total *= 0.9; // 10% discount
}
if (total > 100) {
total -= 10; // additional discount for large orders
}
return { ...order, total };
}
参考リンク
次回予告: Day 9では「実践プロジェクト」に取り組みます。これまで学んだ知識を総動員して、実際のアプリケーションにテストを書いていきましょう!