Fix: Jest Coverage Not Collected — Files Missing from Coverage Report
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 absentOr 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 fineOr TypeScript files don’t show up in coverage at all:
jest --coverage
# All .ts/.tsx files missing from report
# Only .js files appearOr the v8 coverage provider gives different numbers than babel:
# With --coverage-provider=babel: 85% statements
# With --coverage-provider=v8: 71% statementsWhy 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. collectCoverageFrommust 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-jestor@babel/preset-typescriptto be transformed. If transformation fails, the file is skipped. v8vsbabelcoverage providers —babelinstruments source code at the AST level;v8uses Node.js’s built-in coverage. They handle branches differently, producing different numbers.- Exclude patterns not matching — a
coveragePathIgnorePatternsentry 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 visibleFind 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" | sortFix 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=v8Fix 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 blockCommon 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 adefault: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=coverageGitHub 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: trueFix 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 filesReset coverage cache:
# Clear Jest's transform cache (sometimes causes stale coverage data)
jest --clearCache
jest --coverageStill 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.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Jest Setup File Not Working — setupFilesAfterFramework Not Running or Globals Not Applied
How to fix Jest setup file issues — setupFilesAfterFramework vs setupFiles, global mocks not applying, @testing-library/jest-dom matchers, module mocking in setup, and TypeScript setup files.
Fix: Jest Async Test Timeout — Exceeded 5000ms or Test Never Resolves
How to fix Jest async test timeouts — missing await, unresolved Promises, done callback misuse, global timeout configuration, fake timers, and async setup/teardown issues.
Fix: Jest Fake Timers Not Working — setTimeout and setInterval Not Advancing
How to fix Jest fake timers not working — useFakeTimers setup, runAllTimers vs advanceTimersByTime, async timers, React testing with act(), and common timer test mistakes.
Fix: Jest Module Mock Not Working — jest.mock() Has No Effect
How to fix Jest module mocks not working — hoisting behavior, ES module mocks, factory functions, mockReturnValue vs implementation, and clearing mocks between tests.