Skip to content

Fix: Jest Coverage Not Collected — Files Missing from Coverage Report

FixDevs ·

Quick Answer

How to fix Jest coverage not collecting all files — collectCoverageFrom config, coverage thresholds, Istanbul ignore comments, ts-jest setup, and Babel transform issues.

The Problem

Jest’s coverage report is missing files that should be covered:

jest --coverage

# Coverage report shows only tested files, not the whole codebase:
# File                 | % Stmts | % Branch | % Funcs | % Lines
# src/utils/format.ts  |   85.71 |    75.00 |   100.0 |   85.71
#
# But src/utils/validate.ts (zero tests) doesn't appear at all
# — it should show 0% coverage, not be absent

Or coverage is collected but the threshold check fails unexpectedly:

Jest: "global" coverage threshold for statements (80%) not met: 62%
# 62% shows up even though individual files look fine

Or TypeScript files don’t show up in coverage at all:

jest --coverage
# All .ts/.tsx files missing from report
# Only .js files appear

Or the v8 coverage provider gives different numbers than babel:

# With --coverage-provider=babel: 85% statements
# With --coverage-provider=v8:    71% statements

Why This Happens

Jest’s coverage collection works differently from test execution. Key behaviors:

  • Coverage is only collected for imported files by default — if no test imports validate.ts, it doesn’t appear in the coverage report. The file has 0% coverage, but Jest doesn’t know it exists.
  • collectCoverageFrom must be set — this config option tells Jest which files to include in the coverage report, regardless of whether they’re imported by tests.
  • Transform configuration affects coverage — TypeScript files require ts-jest or @babel/preset-typescript to be transformed. If transformation fails, the file is skipped.
  • v8 vs babel coverage providersbabel instruments source code at the AST level; v8 uses Node.js’s built-in coverage. They handle branches differently, producing different numbers.
  • Exclude patterns not matching — a coveragePathIgnorePatterns entry that doesn’t correctly match a file path results in those files being included (and potentially bringing down the average).

Fix 1: Configure collectCoverageFrom

The single most impactful fix — tell Jest to include all source files in coverage, not just the ones imported by tests:

// jest.config.js
module.exports = {
  collectCoverage: true,          // Always collect coverage (or use --coverage flag)
  collectCoverageFrom: [
    'src/**/*.{js,jsx,ts,tsx}',   // All source files
    '!src/**/*.d.ts',             // Exclude type declaration files
    '!src/**/*.stories.{js,ts,tsx}',  // Exclude Storybook stories
    '!src/**/index.{js,ts}',     // Exclude barrel files (optional)
    '!src/**/__mocks__/**',       // Exclude mock files
    '!src/setupTests.{js,ts}',   // Exclude test setup
  ],
  coverageDirectory: 'coverage',
  coverageReporters: ['text', 'lcov', 'html'],   // text = terminal, lcov = CI, html = browser
};

Verify the configuration works:

# Run coverage and check that zero-test files appear at 0%
jest --coverage

# Output should now include ALL source files:
# File                    | % Stmts | % Branch | % Funcs | % Lines
# src/utils/format.ts     |   85.71 |    75.00 |   100.0 |   85.71
# src/utils/validate.ts   |    0.00 |     0.00 |    0.00 |    0.00  ← now visible

Find which files are being excluded:

# Check which files match your collectCoverageFrom patterns
npx jest --coverage --verbose 2>&1 | grep "coverage"

# Or list all source files to compare against the coverage report
find src -name "*.ts" -not -name "*.d.ts" | sort

Fix 2: Fix TypeScript Coverage Collection

TypeScript projects need the transform configured correctly for coverage to work:

With ts-jest:

// jest.config.js
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  collectCoverageFrom: [
    'src/**/*.ts',
    '!src/**/*.d.ts',
  ],
  // ts-jest handles TypeScript transformation — coverage should work automatically
};

With Babel (using @babel/preset-typescript):

// jest.config.js
module.exports = {
  transform: {
    '^.+\\.(t|j)sx?$': 'babel-jest',
  },
  collectCoverageFrom: [
    'src/**/*.{ts,tsx,js,jsx}',
  ],
};

// babel.config.js
module.exports = {
  presets: [
    ['@babel/preset-env', { targets: { node: 'current' } }],
    '@babel/preset-typescript',
    '@babel/preset-react',
  ],
};

Coverage provider — switch to v8 for better TypeScript branch coverage:

// jest.config.js
module.exports = {
  coverageProvider: 'v8',   // Better branch coverage for TypeScript than 'babel'
  // Or: 'babel' (default) — more stable but less accurate branch detection
};
# Or specify via CLI
jest --coverage --coverageProvider=v8

Fix 3: Set and Fix Coverage Thresholds

Coverage thresholds fail the test run if coverage drops below specified percentages:

// jest.config.js
module.exports = {
  coverageThreshold: {
    // Global thresholds — applies to aggregate across all files
    global: {
      statements: 80,
      branches: 70,
      functions: 85,
      lines: 80,
    },

    // Per-file thresholds — applies to each file individually
    './src/utils/critical-payment.ts': {
      statements: 100,
      branches: 100,
      functions: 100,
      lines: 100,
    },

    // Glob pattern thresholds
    './src/utils/**/*.ts': {
      statements: 90,
    },
  },
};

When thresholds fail unexpectedly — check if uncovered files are dragging down the average:

# View full coverage report including 0% files
jest --coverage --coverageReporters=text

# Look for files with very low coverage that are included unexpectedly
# Common causes:
# - Generated files (migrations, schema files) included in collectCoverageFrom
# - Third-party code copied into src/
# - Test fixtures or seed data in src/

Temporarily lower thresholds while increasing coverage:

// Set realistic thresholds based on current coverage
// Run `jest --coverage` first, note the actual percentages
// Set thresholds slightly below current to prevent regression:
coverageThreshold: {
  global: {
    statements: 62,   // Current is 65%, set 3% below to catch regressions
  },
},

Fix 4: Use Istanbul Ignore Comments

Mark specific code blocks that shouldn’t be counted (generated code, impossible branches, defensive checks):

// Ignore the next line
/* istanbul ignore next */
const debugOnly = process.env.NODE_ENV === 'development' ? debugHelper() : null;

// Ignore an entire function
/* istanbul ignore next */
function emergencyFallback() {
  // This code path can't be triggered in tests — defensive only
  process.exit(1);
}

// Ignore a specific branch
function getConfig() {
  return {
    timeout: process.env.TIMEOUT
      ? parseInt(process.env.TIMEOUT)
      : /* istanbul ignore next */ 5000,  // Default branch never hit in tests
  };
}

// Ignore entire file (put at top of file)
/* istanbul ignore file */

// For v8 coverage provider — use c8 ignore comments instead
/* c8 ignore next */
/* c8 ignore next 3 */ // Ignore next 3 lines
/* c8 ignore start */ ... /* c8 ignore stop */ // Ignore a block

Common legitimate uses of ignore comments:

  • Platform-specific code paths (if (process.platform === 'win32'))
  • Development-only code that’s gated by env variables
  • /* istanbul ignore next */ on a default: branch that should never be reached
  • Error handling for truly impossible conditions

Fix 5: Fix Coverage for React Components

React component coverage has some quirks — particularly with JSX branches:

// Component with conditional rendering
function UserCard({ user, isLoading }) {
  if (isLoading) {
    return <Spinner />;    // Branch: isLoading = true
  }

  return (
    <div>
      <h2>{user.name}</h2>
      {user.isPremium && <PremiumBadge />}  {/* Branch: isPremium true/false */}
    </div>
  );
}

// Test both branches for full coverage:
test('shows spinner when loading', () => {
  render(<UserCard isLoading={true} user={null} />);
  expect(screen.getByRole('status')).toBeInTheDocument();
});

test('shows user card', () => {
  render(<UserCard isLoading={false} user={{ name: 'Alice', isPremium: false }} />);
  expect(screen.getByText('Alice')).toBeInTheDocument();
});

test('shows premium badge for premium users', () => {
  render(<UserCard isLoading={false} user={{ name: 'Alice', isPremium: true }} />);
  expect(screen.getByTestId('premium-badge')).toBeInTheDocument();
});

Coverage for custom hooks:

// hooks/useLocalStorage.ts
export function useLocalStorage<T>(key: string, initialValue: T) {
  try {
    const item = window.localStorage.getItem(key);
    return item ? JSON.parse(item) : initialValue;
  } catch {
    return initialValue;   // Error branch — often missed in tests
  }
}

// Test — cover the error branch
test('returns initial value when localStorage throws', () => {
  jest.spyOn(Storage.prototype, 'getItem').mockImplementation(() => {
    throw new Error('Storage quota exceeded');
  });

  const { result } = renderHook(() => useLocalStorage('key', 'default'));
  expect(result.current).toBe('default');
});

Fix 6: Coverage in Monorepos and Complex Projects

In monorepos or projects with multiple Jest configurations, coverage can be collected per-package or aggregated:

// Root jest.config.js for a monorepo
module.exports = {
  projects: [
    '<rootDir>/packages/api',
    '<rootDir>/packages/ui',
    '<rootDir>/packages/shared',
  ],

  // Collect coverage from ALL packages
  collectCoverageFrom: [
    '<rootDir>/packages/*/src/**/*.{ts,tsx}',
    '!<rootDir>/packages/*/src/**/*.d.ts',
  ],

  coverageDirectory: '<rootDir>/coverage',
};

Merge coverage from multiple test runs:

# Run tests in each package with coverage output in JSON format
jest --coverage --coverageReporters=json --coverageDirectory=./coverage/api
jest --coverage --coverageReporters=json --coverageDirectory=./coverage/ui

# Merge using nyc
npx nyc merge coverage coverage/merged.json
npx nyc report --reporter=html --temp-dir=coverage

GitHub Actions — upload coverage to Codecov or similar:

- name: Run tests with coverage
  run: jest --coverage --coverageReporters=lcov

- name: Upload coverage to Codecov
  uses: codecov/codecov-action@v4
  with:
    files: ./coverage/lcov.info
    fail_ci_if_error: true

Fix 7: Debug Coverage Collection Issues

When coverage numbers look wrong or files are missing:

# Run with --verbose to see which files are transformed
jest --coverage --verbose 2>&1 | head -50

# Check which files are included/excluded by your collectCoverageFrom pattern
# Use a one-off script to verify:
node -e "
const glob = require('glob');
const files = glob.sync('src/**/*.{ts,tsx}', { ignore: ['src/**/*.d.ts'] });
console.log('Files that would be included:', files.length);
files.forEach(f => console.log(' ', f));
"

# Check if a specific file appears in coverage
jest --coverage --coverageReporters=json-summary
cat coverage/coverage-summary.json | python3 -m json.tool | grep "src/utils/validate"

Coverage is 0% for a file you know has tests:

# Check if the file is being transformed correctly
jest --showConfig 2>&1 | grep -A 5 "transform"

# Verify the transform pattern matches your file extension
# A transform entry like "^.+\\.js$" won't match .ts files

Reset coverage cache:

# Clear Jest's transform cache (sometimes causes stale coverage data)
jest --clearCache
jest --coverage

Still Not Working?

moduleNameMapper hiding real coverage — if moduleNameMapper redirects imports to mock files, the real implementation may not be loaded during tests at all. Coverage for the real file is 0% even though the mock is tested. Use jest.unmock() or jest.requireActual() in specific tests that should cover the real implementation.

Source maps and coverage — if your project uses source maps (TypeScript → JS), coverage is reported against the source file (TypeScript). If source maps are missing or wrong, coverage may be reported against the compiled JavaScript instead. Ensure sourceMap: true in tsconfig.json.

--passWithNoTests hiding coverage failures--passWithNoTests lets Jest succeed even when no tests run, which also skips coverage collection. Remove this flag in CI coverage jobs.

Coverage for dynamic imports — code split via import() dynamic imports may not be covered by default with Babel. The v8 coverage provider handles dynamic imports better.

For related testing issues, see Fix: Jest Fake Timers Not Working and Fix: Jest Module Mock Not Working.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles