10日で覚えるJestDay 8: カバレッジとデバッグ

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 未閉鎖ハンドルを検出

重要ポイント

  1. カバレッジは「テストされていない箇所を見つけるツール」であり、テストの品質指標ではない
  2. カバレッジしきい値を設定してCI/CDで品質ゲートとして活用する
  3. デバッグは console.log から始めて、必要に応じて debugger やVS Code連携へ
  4. --bail--verbose--runInBand を組み合わせてデバッグ効率を上げる
  5. よくあるエラーのパターンを覚えて、素早く対処する

練習問題

問題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では「実践プロジェクト」に取り組みます。これまで学んだ知識を総動員して、実際のアプリケーションにテストを書いていきましょう!