T-CREATOR

Jest のカバレッジが 0% になる原因と対処:sourceMap/babel 設定の落とし穴

Jest のカバレッジが 0% になる原因と対処:sourceMap/babel 設定の落とし穴

Jest でテストを実行してカバレッジレポートを生成したのに、なぜか全てのファイルで 0% と表示される経験はありませんか?テストは正常に動作しているのに、カバレッジだけが取得できない状況は非常に困惑しますよね。

この問題の多くは、ソースマップや Babel の設定に起因しています。本記事では、Jest のカバレッジが 0% になってしまう根本原因と、その解決方法を段階的に解説していきます。同じ問題で悩んでいる方にとって、きっと解決の糸口が見つかるはずです。

背景

Jest のカバレッジ計測の仕組み

Jest は、コードカバレッジを計測するために内部的に Istanbul(現在は NYC)というツールを使用しています。カバレッジ計測の基本的な流れは以下の通りです。

mermaidflowchart TB
  source["ソースコード<br/>(TypeScript/ES6+)"] --> transform["変換プロセス<br/>(Babel/ts-jest)"]
  transform --> instrumented["計測用コードの挿入<br/>(Istanbul)"]
  instrumented --> execute["テスト実行"]
  execute --> coverage["カバレッジデータ収集"]
  coverage --> sourcemap["ソースマップで<br/>元コードに逆マッピング"]
  sourcemap --> report["カバレッジレポート生成"]

上記の図から分かるように、カバレッジ計測では「変換されたコード」と「元のソースコード」を正確に紐づける必要があります。この紐づけに使われるのがソースマップです。

TypeScript と Babel による変換

モダンな JavaScript プロジェクトでは、TypeScript や ES6+ の構文を使用するのが一般的になりました。これらのコードは、実行前に以下のような変換を経ます。

typescript// 変換前:TypeScript(元のソースコード)
interface User {
  id: number;
  name: string;
}

const getUser = (id: number): User => {
  return { id, name: 'test user' };
};
javascript// 変換後:JavaScript(実行されるコード)
'use strict';
var getUser = function (id) {
  return { id: id, name: 'test user' };
};

Jest はこの変換後のコードに対してカバレッジ計測用のコードを挿入します。しかし、最終的なレポートでは元の TypeScript コードに対してカバレッジを表示したいですよね。そのためにソースマップが必要になるのです。

ソースマップの役割

ソースマップは、変換後のコードと変換前のコードの対応関係を記録したファイルです。これにより、以下のような情報を正確に保持できます。

#項目説明
1行番号のマッピング変換後の 10 行目が元コードの 5 行目に対応
2列番号のマッピング精密な位置情報の保持
3ファイル名の対応複数ファイルの結合時も追跡可能
4変数名のマッピングminify 後も元の変数名を特定

ソースマップが正しく生成されていないと、Jest はカバレッジ情報を元のソースコードに逆マッピングできず、結果として 0% と表示されてしまいます。

課題

カバレッジが 0% になる主な症状

Jest のカバレッジが 0% になる問題は、以下のような症状として現れます。

bash# テスト実行とカバレッジ生成
$ yarn test --coverage

# 出力例
PASS  src/utils/helper.test.ts
  ✓ should return correct value (5 ms)

----------|---------|----------|---------|---------|-------------------
File      | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files |       0 |        0 |       0 |       0 |
----------|---------|----------|---------|---------|-------------------

テストは全て成功しているのに、カバレッジは全て 0% という不可解な状況です。さらに、coverage​/​lcov-report​/​index.html を開いても、ファイル一覧が空だったり、正しく表示されなかったりします。

問題発生の主な原因

カバレッジが 0% になる原因は、大きく分けて以下の 4 つに分類されます。

mermaidflowchart LR
  problem["カバレッジ 0%<br/>の問題"] --> cause1["原因1<br/>ソースマップ未生成"]
  problem --> cause2["原因2<br/>Babel 設定の誤り"]
  problem --> cause3["原因3<br/>変換パスの不一致"]
  problem --> cause4["原因4<br/>Jest 設定の不足"]

  cause1 --> detail1["tsconfig.json<br/>sourceMap: false"]
  cause2 --> detail2["babel.config.js<br/>sourceMaps オプション欠如"]
  cause3 --> detail3["transform パターン<br/>の設定ミス"]
  cause4 --> detail4["collectCoverageFrom<br/>の指定漏れ"]

それぞれの原因について、具体的に見ていきましょう。

原因 1:ソースマップが生成されていない

TypeScript プロジェクトで最も多いのが、tsconfig.json でソースマップの生成が無効になっているケースです。

json// tsconfig.json(問題のある設定)
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "sourceMap": false, // これが原因!
    "outDir": "./dist"
  }
}

sourceMapfalse になっていると、.js.map ファイルが生成されません。Jest はこのマップファイルを参照できないため、カバレッジ情報を元のコードに紐づけられなくなります。

原因 2:Babel のソースマップ設定が不適切

Babel を使用している場合、babel.config.js.babelrc でソースマップの生成設定が必要です。

javascript// babel.config.js(問題のある設定)
module.exports = {
  presets: [
    '@babel/preset-env',
    '@babel/preset-typescript',
  ],
  // sourceMaps の設定がない!
};

Babel のデフォルトではソースマップが生成されないため、明示的に設定する必要があります。特に @babel​/​preset-typescript を使用している場合は要注意です。

原因 3:transform 設定とファイルパスの不一致

Jest の transform 設定が適切でないと、一部のファイルが変換されずにカバレッジ計測の対象から漏れてしまいます。

javascript// jest.config.js(問題のある設定)
module.exports = {
  transform: {
    '^.+\\.ts$': 'ts-jest', // .tsx ファイルが対象外!
  },
  collectCoverageFrom: [
    'src/**/*.{ts,tsx}', // tsx も収集対象なのに変換されない
  ],
};

この例では、.tsx ファイルは collectCoverageFrom で収集対象になっているものの、transform の正規表現パターンにマッチしないため、適切に変換されません。

原因 4:collectCoverageFrom の設定不足

collectCoverageFrom の設定が不十分だと、Jest はどのファイルをカバレッジ対象とすべきか判断できません。

javascript// jest.config.js(問題のある設定)
module.exports = {
  // collectCoverageFrom の設定がない
  testMatch: ['**/*.test.ts'],
};

collectCoverageFrom を指定しないと、テストファイル自体のカバレッジしか計測されず、実際のソースコードは 0% と表示されてしまいます。

解決策

解決手順の全体像

カバレッジ 0% 問題を解決するには、以下の順序で設定を確認・修正していきます。

#ステップ確認項目所要時間目安
1TypeScript 設定tsconfig.json の sourceMap2 分
2Babel 設定babel.config.js の sourceMaps3 分
3Jest 変換設定jest.config.js の transform5 分
4カバレッジ収集設定collectCoverageFrom の指定3 分
5動作確認カバレッジレポートの生成5 分

それでは、各ステップを詳しく見ていきましょう。

ステップ 1:TypeScript のソースマップ設定

まず、tsconfig.json でソースマップの生成を有効にします。

json{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "lib": ["ES2020"],
    "jsx": "react",
    "sourceMap": true, // ソースマップを有効化
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": [
    "node_modules",
    "dist",
    "**/*.test.ts",
    "**/*.test.tsx"
  ]
}

sourceMap: true を設定することで、TypeScript コンパイラは .js.map ファイルを生成するようになります。これが最も基本的な設定です。

inlineSourceMap というオプションもありますが、これは .js.map ファイルを別途作成せず、変換後の JS ファイルの末尾にソースマップを埋め込む方式です。

json{
  "compilerOptions": {
    "inlineSourceMap": true, // インライン形式も可能
    "inlineSources": true // 元のソースコードも埋め込む
  }
}

ただし、Jest のカバレッジ計測では sourceMap: true の方が安定して動作するケースが多いため、まずはこちらを試すことをお勧めします。

ステップ 2:Babel のソースマップ設定

Babel を使用している場合は、babel.config.js または .babelrcsourceMaps オプションを設定します。

javascript// babel.config.js
module.exports = {
  presets: [
    [
      '@babel/preset-env',
      {
        targets: {
          node: 'current', // Node.js の現在のバージョンをターゲット
        },
      },
    ],
    [
      '@babel/preset-typescript',
      {
        isTSX: true, // TSX ファイルのサポート
        allExtensions: true, // 全ての拡張子で TypeScript 構文を許可
      },
    ],
  ],
  // ソースマップの生成を有効化(重要)
  sourceMaps: 'both', // inline と external の両方を生成
};

sourceMaps オプションには以下の値が指定できます。

#設定値説明推奨度
1true または 'inline'インラインソースマップを生成★★☆
2'both'インラインと外部ファイルの両方を生成★★★
3falseソースマップを生成しない☆☆☆

Jest のカバレッジ計測では、'both' を指定するのが最も確実です。環境によって挙動が異なる場合があるため、両方生成しておくと安心ですね。

ステップ 3:Jest の transform 設定

jest.config.js で、全ての TypeScript/TSX ファイルが適切に変換されるように設定します。

javascript// jest.config.js
module.exports = {
  // テスト環境の設定
  testEnvironment: 'node',

  // ルートディレクトリの指定
  roots: ['<rootDir>/src'],

  // 変換設定(重要)
  transform: {
    '^.+\\.(ts|tsx)$': [
      'ts-jest',
      {
        tsconfig: {
          sourceMap: true, // ts-jest にもソースマップ生成を指示
        },
      },
    ],
  },
};

transform の正規表現パターン ^.+\\.(ts|tsx)$ により、.ts.tsx の両方のファイルが ts-jest で変換されます。

ts-jest を使用する場合、トランスフォーマーのオプションとしても sourceMap: true を指定することで、より確実にソースマップが生成されます。

Babel を使用する場合の設定は以下のようになります。

javascript// jest.config.js(Babel 使用時)
module.exports = {
  testEnvironment: 'node',
  roots: ['<rootDir>/src'],

  transform: {
    '^.+\\.(ts|tsx|js|jsx)$': [
      'babel-jest',
      {
        // Babel にソースマップ生成を指示
        sourceMaps: 'both',
      },
    ],
  },
};

babel-jest を使用する場合も、オプションで sourceMaps を指定することが重要です。babel.config.js の設定と合わせて、二重に指定しておくと確実ですね。

ステップ 4:カバレッジ収集対象の設定

collectCoverageFrom を使って、カバレッジを収集するファイルを明示的に指定します。

javascript// jest.config.js
module.exports = {
  testEnvironment: 'node',
  roots: ['<rootDir>/src'],

  transform: {
    '^.+\\.(ts|tsx)$': [
      'ts-jest',
      {
        tsconfig: {
          sourceMap: true,
        },
      },
    ],
  },

  // カバレッジ収集対象の指定(重要)
  collectCoverageFrom: [
    'src/**/*.{ts,tsx}', // 全ての TypeScript ファイル
    '!src/**/*.test.{ts,tsx}', // テストファイルは除外
    '!src/**/*.spec.{ts,tsx}', // スペックファイルも除外
    '!src/**/__tests__/**', // __tests__ ディレクトリは除外
    '!src/**/index.{ts,tsx}', // エントリーポイントは除外(任意)
  ],
};

collectCoverageFrom では glob パターンを使用して、カバレッジ計測の対象を柔軟に指定できます。! で始まるパターンは除外を意味します。

テストファイル自体はカバレッジの対象にする必要がないため、明示的に除外しておくと良いでしょう。

ステップ 5:カバレッジしきい値の設定

カバレッジが正しく取得できるようになったら、最低限のカバレッジ率を設定しておくことをお勧めします。

javascript// jest.config.js
module.exports = {
  // 前述の設定に加えて...

  // カバレッジしきい値の設定
  coverageThreshold: {
    global: {
      branches: 80, // 分岐カバレッジ 80% 以上
      functions: 80, // 関数カバレッジ 80% 以上
      lines: 80, // 行カバレッジ 80% 以上
      statements: 80, // 文カバレッジ 80% 以上
    },
  },

  // カバレッジレポートの形式
  coverageReporters: [
    'text', // コンソール出力
    'text-summary', // サマリー出力
    'html', // HTML レポート
    'lcov', // lcov 形式(CI/CD で使用)
  ],
};

coverageThreshold を設定しておくと、カバレッジが基準を下回った場合にテストが失敗するようになります。これにより、コード品質の維持がしやすくなりますね。

具体例

実際のプロジェクト構成

実際のプロジェクトで、カバレッジ 0% 問題を解決する手順を見ていきましょう。以下のような Next.js + TypeScript プロジェクトを想定します。

luamy-app/
├── src/
│   ├── components/
│   │   ├── Button.tsx
│   │   └── Button.test.tsx
│   ├── utils/
│   │   ├── format.ts
│   │   └── format.test.ts
│   └── pages/
│       └── index.tsx
├── babel.config.js
├── jest.config.js
├── tsconfig.json
└── package.json

このプロジェクトで、カバレッジが 0% になっていたケースの解決方法を順を追って説明します。

問題が発生していた状態

最初、以下のような設定でテストを実行したところ、全てのカバレッジが 0% になっていました。

javascript// jest.config.js(問題があった設定)
module.exports = {
  testEnvironment: 'jsdom',
  roots: ['<rootDir>/src'],
  testMatch: ['**/*.test.ts', '**/*.test.tsx'],
};

この設定では、transformcollectCoverageFrom も指定されていません。テストを実行すると以下のような結果になります。

bash$ yarn test --coverage

 PASS  src/utils/format.test.ts
 PASS  src/components/Button.test.tsx

----------|---------|----------|---------|---------|-------------------
File      | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files |       0 |        0 |       0 |       0 |
----------|---------|----------|---------|---------|-------------------

Test Suites: 2 passed, 2 total
Tests:       5 passed, 5 total

テストは成功しているのに、カバレッジは全く取得できていない状態です。

エラーログの確認

詳細なログを出力してみると、以下のようなメッセージが見つかりました。

bash$ yarn test --coverage --verbose

# 出力の一部
No coverage information was collected for src/utils/format.ts
Unable to find source map for src/components/Button.tsx

これらのメッセージから、ソースマップが見つからないことが問題だと分かります。

解決手順 1:tsconfig.json の修正

まず、TypeScript のソースマップ生成を有効にします。

json{
  "compilerOptions": {
    "target": "ES2020",
    "lib": ["DOM", "DOM.Iterable", "ES2020"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    },
    // ソースマップを有効化(追加)
    "sourceMap": true
  },
  "include": ["src"],
  "exclude": [
    "node_modules",
    "**/*.test.ts",
    "**/*.test.tsx"
  ]
}

"sourceMap": true を追加することで、.ts.map ファイルが生成されるようになります。

解決手順 2:babel.config.js の修正

Next.js プロジェクトでは Babel も使用されているため、こちらも修正します。

javascript// babel.config.js
module.exports = {
  presets: [
    'next/babel', // Next.js の推奨 Babel 設定
  ],
  // テスト環境でのみソースマップを生成
  env: {
    test: {
      presets: [
        [
          'next/babel',
          {
            'preset-env': {
              modules: 'commonjs', // Jest は CommonJS が必要
            },
          },
        ],
      ],
      // ソースマップを生成(追加)
      sourceMaps: 'both',
    },
  },
};

env.test セクションを追加して、テスト実行時のみソースマップを生成するようにしています。本番ビルドへの影響を最小限に抑えられますね。

解決手順 3:jest.config.js の完全な設定

最後に、Jest の設定を完全なものにします。

javascript// jest.config.js(完全版)
const nextJest = require('next/jest');

// Next.js の設定を読み込む関数
const createJestConfig = nextJest({
  dir: './', // Next.js アプリのルートディレクトリ
});

// Jest のカスタム設定
const customJestConfig = {
  // テスト環境の設定
  testEnvironment: 'jest-environment-jsdom',

  // テストファイルのルート
  roots: ['<rootDir>/src'],

  // テストファイルのパターン
  testMatch: [
    '**/__tests__/**/*.(test|spec).[jt]s?(x)',
    '**/?(*.)+(test|spec).[jt]s?(x)',
  ],
};

ここまでで基本設定は完了です。次に、カバレッジ関連の設定を追加します。

javascript// jest.config.js(続き)
const customJestConfig = {
  // 前述の設定...

  // カバレッジ収集対象の指定
  collectCoverageFrom: [
    'src/**/*.{js,jsx,ts,tsx}',
    '!src/**/_app.tsx', // Next.js の _app は除外
    '!src/**/_document.tsx', // Next.js の _document は除外
    '!src/**/*.stories.tsx', // Storybook ファイルは除外
    '!src/**/*.test.{ts,tsx}', // テストファイルは除外
    '!src/**/__tests__/**', // テストディレクトリは除外
  ],

  // カバレッジレポートの形式
  coverageReporters: ['text', 'lcov', 'html'],

  // カバレッジディレクトリ
  coverageDirectory: 'coverage',
};

さらに、変換設定とモジュール解決の設定を追加します。

javascript// jest.config.js(続き)
const customJestConfig = {
  // 前述の設定...

  // ファイル変換の設定
  transform: {
    '^.+\\.(ts|tsx)$': [
      'babel-jest',
      {
        sourceMaps: 'both', // ソースマップを生成
      },
    ],
  },

  // モジュール名のマッピング(パスエイリアス対応)
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1',
  },

  // セットアップファイル
  setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
};

// 設定をエクスポート
module.exports = createJestConfig(customJestConfig);

next​/​jest を使用すると、Next.js に最適化された設定が自動的に適用されるため、設定ファイルがシンプルになります。

動作確認

設定を修正した後、再度テストを実行してみます。

bash$ yarn test --coverage

 PASS  src/utils/format.test.ts
  format function
    ✓ should format number with commas (3 ms)
    ✓ should format date correctly (2 ms)

 PASS  src/components/Button.test.tsx
  Button component
    ✓ should render correctly (15 ms)
    ✓ should handle click event (5 ms)
    ✓ should be disabled when prop is set (3 ms)

----------|---------|----------|---------|---------|-------------------
File      | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files |   85.71 |    83.33 |      80 |   85.71 |
 format.ts|     100 |      100 |     100 |     100 |
 Button.tsx|   83.33 |       75 |   66.67 |   83.33 | 25-27
----------|---------|----------|---------|---------|-------------------

Test Suites: 2 passed, 2 total
Tests:       5 passed, 5 total
Snapshots:   0 total
Time:        2.567 s

カバレッジが正しく表示されるようになりました!各ファイルのカバレッジ率が具体的に表示され、Button.tsx の 25-27 行目がカバーされていないことも分かります。

HTML レポートの確認

生成された HTML レポートも確認してみましょう。

bash# HTML レポートを開く
$ open coverage/lcov-report/index.html

ブラウザで開くと、以下のような詳細なレポートが表示されます。

mermaidflowchart TB
  index["カバレッジ<br/>トップページ"] --> filelist["ファイル一覧<br/>各ファイルの<br/>カバレッジ率"]
  filelist --> detail["詳細ページ<br/>行ごとの<br/>カバレッジ状況"]
  detail --> uncovered["未カバー行<br/>ハイライト表示"]

  style uncovered fill:#ffcccc
  style detail fill:#ccffcc

レポートでは、カバーされた行は緑色、カバーされていない行は赤色でハイライト表示されます。視覚的に分かりやすいため、テストの追加が必要な箇所をすぐに特定できますね。

トラブルシューティング:それでもカバレッジが 0% の場合

上記の設定をしてもまだカバレッジが 0% の場合、以下を確認してください。

キャッシュのクリア

Jest はキャッシュを使用しているため、設定変更が反映されない場合があります。

bash# Jest のキャッシュをクリア
$ yarn jest --clearCache

# node_modules を削除して再インストール
$ rm -rf node_modules
$ yarn install

# 再度テストを実行
$ yarn test --coverage

キャッシュが原因の場合、これで解決することが多いです。

ts-jest の詳細ログを有効化

ts-jest の詳細ログを出力して、変換プロセスを確認します。

javascript// jest.config.js
module.exports = {
  // 前述の設定...

  transform: {
    '^.+\\.(ts|tsx)$': [
      'ts-jest',
      {
        tsconfig: {
          sourceMap: true,
        },
        // 詳細ログを有効化
        diagnostics: {
          warnOnly: false,
          pretty: true,
        },
      },
    ],
  },

  // Jest の詳細ログも有効化
  verbose: true,
};

詳細ログを確認することで、どこで問題が発生しているか特定しやすくなります。

カバレッジプロバイダーの変更

Jest v28 以降では、カバレッジプロバイダーとして v8 も選択できます。

javascript// jest.config.js
module.exports = {
  // 前述の設定...

  // カバレッジプロバイダーを V8 に変更
  coverageProvider: 'v8', // デフォルトは 'babel'
};

v8 プロバイダーは、Node.js の V8 エンジンの組み込みカバレッジ機能を使用します。babel プロバイダーで問題がある場合、こちらを試してみる価値があります。

ただし、v8 プロバイダーは一部の環境で挙動が異なる場合があるため、プロジェクトの要件に合わせて選択してください。

パフォーマンス最適化

カバレッジ計測は処理が重くなりがちです。以下の設定で高速化できます。

javascript// jest.config.js
module.exports = {
  // 前述の設定...

  // 並列実行の最大ワーカー数
  maxWorkers: '50%', // CPU コアの 50% を使用

  // テストのタイムアウト
  testTimeout: 10000, // 10 秒

  // カバレッジ収集対象を最小限に
  collectCoverageFrom: [
    'src/**/*.{ts,tsx}',
    '!src/**/*.test.{ts,tsx}',
    '!src/**/*.stories.{ts,tsx}',
    '!src/**/types/**', // 型定義ファイルは除外
    '!src/**/constants/**', // 定数ファイルは除外(任意)
  ],
};

型定義ファイルや定数ファイルなど、テストが不要なファイルは除外することで、カバレッジ計測の時間を短縮できます。

まとめ

Jest のカバレッジが 0% になる問題は、主にソースマップの設定不足が原因です。本記事で解説した内容をまとめます。

重要なポイント

#項目設定内容ファイル
1TypeScript"sourceMap": truetsconfig.json
2BabelsourceMaps: 'both'babel.config.js
3Jest 変換transform の設定jest.config.js
4カバレッジ収集collectCoverageFromjest.config.js

これらの設定を正しく行うことで、カバレッジが正常に計測されるようになります。

チェックリスト

問題が発生した際は、以下の順序で確認してください。

  1. tsconfig.jsonsourceMap または inlineSourceMaptrue になっているか
  2. babel.config.jssourceMaps オプションが設定されているか
  3. jest.config.jstransform で全ての対象ファイルが変換されるか
  4. jest.config.jscollectCoverageFrom でカバレッジ対象が指定されているか
  5. キャッシュをクリアして再実行したか

これらを順番にチェックすれば、ほとんどのケースで問題は解決できるはずです。

トラブル時の対処法

それでも解決しない場合は、以下を試してみてください。

  • Jest と関連パッケージを最新版にアップデート
  • coverageProvider'v8' に変更してみる
  • 詳細ログを有効化して、変換プロセスを確認する
  • 最小限の設定から始めて、少しずつ追加していく

カバレッジが正しく取得できるようになると、テストの品質を定量的に把握できます。コードレビューでも「この部分のテストが不足している」といった指摘がしやすくなり、チーム全体のコード品質向上につながりますね。

Jest のカバレッジ設定は一度正しく構築してしまえば、その後は安定して動作します。本記事の内容が、皆さんのプロジェクトでのカバレッジ計測に役立てば幸いです。

関連リンク

;