Fix: Jest Coverage Not Collected — Files Missing from Coverage Report
Part of: JavaScript & TypeScript Errors
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).
The coverage provider difference deserves deeper explanation. The babel provider (Istanbul) rewrites your source code to insert counter variables at every branch and statement, then counts which ones execute. The v8 provider uses V8’s built-in code coverage API, which tracks execution at the bytecode level without modifying source. This means v8 sees branches that babel doesn’t (like optional chaining ?. and nullish coalescing ??), often reporting lower branch coverage. The difference can be 10-20% for codebases that use modern JavaScript syntax heavily. Jest 27 defaulted to babel; Jest 29 still defaults to babel but the v8 provider is now stable and recommended for new projects.
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=v8Jest version differences with coverage providers: Jest 27 introduced v8 as an experimental provider. Jest 28 stabilized it but changed how source maps interact with coverage, which sometimes causes line numbers in the coverage report to be off by one. Jest 29 fixed most source map issues with v8, but introduced a new behavior where coveragePathIgnorePatterns is checked against the absolute path instead of the relative path, breaking patterns that worked in Jest 28. Check your Jest version and adjust patterns accordingly.
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 blockIstanbul vs c8 ignore comments: If you switch from coverageProvider: 'babel' to coverageProvider: 'v8', your /* istanbul ignore */ comments stop working. The v8 provider uses c8 under the hood, which only recognizes /* c8 ignore */ comments. If you need to support both providers (e.g., during a migration), you can add both comments, but this is rarely necessary. Pick one provider and stick with it across the project.
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 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 CI Pipelines
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:
- 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: trueDocker volume mount coverage paths: When running Jest inside Docker and mounting the coverage output to the host, paths in the coverage report reference the container’s filesystem (e.g., /app/src/utils.ts), not the host’s. This breaks tools like Codecov that try to match coverage paths to source files. Fix by setting coverageDirectory to a mounted volume path and using --rootDir to align paths:
# Docker run with coverage output mounted to host
docker run -v $(pwd)/coverage:/app/coverage my-test-image \
npx jest --coverage --coverageDirectory=/app/coverageMonorepo coverage aggregation pitfall: When using Jest projects (multiple configs), each project collects coverage independently. If package A imports a utility from package B, the utility’s coverage is only counted in package A’s report, not package B’s. This can cause package B’s coverage to appear lower than expected. Use moduleNameMapper or moduleDirectories to ensure each package’s own tests cover its own code.
Fix 7: Vitest Coverage Comparison
If you’re considering migrating from Jest to Vitest, or using both in a monorepo, understand the coverage differences:
// vitest.config.ts — coverage with c8 or istanbul
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
coverage: {
provider: 'v8', // or 'istanbul'
include: ['src/**/*.ts'],
exclude: ['src/**/*.d.ts', 'src/**/*.test.ts'],
reporter: ['text', 'lcov', 'html'],
thresholds: {
statements: 80,
branches: 70,
functions: 85,
lines: 80,
},
},
},
});Key differences from Jest coverage:
- Vitest’s
v8provider usesc8directly, while Jest wrapsv8coverage through its own transformer layer. This means the same codebase can produce slightly different coverage numbers under Jestv8and Vitestv8. - Vitest reports coverage for
import()dynamic imports more accurately than Jest’sbabelprovider. - Vitest’s
includeoption is equivalent to Jest’scollectCoverageFrom, but uses Vite’s glob syntax (picomatch) rather than Jest’s glob syntax (micromatch). Patterns like!src/**/*.d.tswork the same way, but edge cases with brace expansion differ. - Istanbul ignore comments (
/* istanbul ignore next */) work with Vitest’sistanbulprovider but not itsv8provider, matching Jest’s behavior.
Fix 8: 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 to 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.
Coverage drops after upgrading Jest — moving from Jest 27 to 28 or 29 can cause unexpected coverage changes. Jest 28 changed how coveragePathIgnorePatterns interacts with collectCoverageFrom (ignore patterns now take precedence). Jest 29 changed the default source map handling for the v8 provider, which can shift branch coverage by a few percent. Pin your Jest version in CI and compare coverage reports before and after upgrading.
GitHub Actions parallel test coverage — when splitting tests across parallel CI jobs (using --shard), each job produces partial coverage. You must merge the coverage files after all shards complete. Use nyc merge or Codecov’s built-in merge functionality to combine the partial reports into a single coverage view.
For related testing issues, see Fix: Jest Fake Timers Not Working, Fix: Jest Module Mock Not Working, Fix: Jest ESM Error, and Fix: Jest Cannot Find Module.
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.