Day 8: Coverage and Debugging
What You'll Learn Today
- Code coverage concepts (line, branch, function, statement)
- Running and reading coverage reports
- Setting coverage thresholds
- What coverage does and doesn't tell you
- Debugging techniques (console.log, debugger, VS Code integration)
- Useful Jest CLI options and configuration
- Troubleshooting common Jest errors
What Is Code Coverage?
Code coverage measures which parts of your source code are executed during tests. It provides an objective metric for understanding how thoroughly your code is tested.
flowchart TB
subgraph Coverage["Four Coverage Metrics"]
STMT["Statements"]
BRANCH["Branches"]
FUNC["Functions"]
LINE["Lines"]
end
STMT -->|"Executed statements"| S1["% of all statements\nthat were run"]
BRANCH -->|"Decision paths"| S2["% of if/else paths\nthat were taken"]
FUNC -->|"Called functions"| S3["% of functions\nthat were invoked"]
LINE -->|"Executed lines"| S4["% of lines\nthat were run"]
style Coverage fill:#3b82f6,color:#fff
The Four Coverage Metrics
| Metric | Description | Example |
|---|---|---|
| Statements | Percentage of executed statements | Was const x = 1; executed? |
| Branches | Percentage of decision paths taken | Were both if and else paths visited? |
| Functions | Percentage of called functions | Was each defined function invoked at least once? |
| Lines | Percentage of executed lines | Was each line run? |
A Concrete Example
Consider the following code and how coverage is measured:
// 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);
});
With only this test, coverage looks like:
| Metric | Value | Reason |
|---|---|---|
| Statements | 62.5% | Lines 5 and 7 not executed |
| Branches | 33.3% | Only 1 of 3 branches taken |
| Functions | 100% | calculate was called |
| Lines | 62.5% | Lines 5 and 7 not executed |
Running Coverage Reports
The --coverage Flag
npx jest --coverage
This produces a table in your terminal:
----------|---------|----------|---------|---------|-------------------
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 Reports
When you run coverage, Jest generates an HTML report in the coverage/ directory.
npx jest --coverage
open coverage/lcov-report/index.html
flowchart LR
subgraph Report["HTML Coverage Report"]
INDEX["index.html\nOverall Summary"]
FILE["Per-file Pages\nLine-by-line Detail"]
COLOR["Color Coding"]
end
INDEX --> FILE
FILE --> COLOR
COLOR --> GREEN["Green: Covered"]
COLOR --> RED["Red: Uncovered"]
COLOR --> YELLOW["Yellow: Partially Covered"]
style Report fill:#8b5cf6,color:#fff
style GREEN fill:#22c55e,color:#fff
style RED fill:#ef4444,color:#fff
style YELLOW fill:#f59e0b,color:#fff
In the HTML report you will see:
- Green: Code that was executed by tests
- Red: Code that was never executed
- Yellow: Partially covered code (e.g., only one side of a branch)
xNnext to line numbers: How many times that line was executed
Setting Coverage Thresholds
You can enforce minimum coverage levels in jest.config.js. Tests will fail if coverage drops below the threshold.
// jest.config.js
module.exports = {
collectCoverage: true,
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
};
TypeScript version (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;
Per-Path Thresholds
You can set different thresholds for specific files or directories.
// 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,
},
},
};
Specifying Coverage Collection Targets
// 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
],
};
What Coverage Does and Doesn't Tell You
flowchart TB
subgraph Good["What Coverage Tells You"]
G1["Which code is\nnot tested"]
G2["A rough measure\nof test thoroughness"]
G3["Areas with high\nregression risk"]
end
subgraph Bad["What Coverage Doesn't Tell You"]
B1["Quality of assertions\nor test logic"]
B2["Whether edge cases\nare covered"]
B3["Whether tests verify\ncorrect business logic"]
end
style Good fill:#22c55e,color:#fff
style Bad fill:#ef4444,color:#fff
High Coverage β High Quality
// 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 assertions
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);
});
Best Practice: Use coverage as a tool for finding untested code, not as a quality metric. Rather than chasing 100% coverage, focus on ensuring critical business logic is well tested.
Debugging Techniques
1. console.log Debugging
The simplest debugging approach.
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: When running many tests,
console.logoutput can get buried. Combine it with the--verboseflag for easier identification.
2. debugger and Node.js Inspector
node --inspect-brk node_modules/.bin/jest --runInBand
Open chrome://inspect in your browser to connect to the Node.js debug session.
test('debug with debugger', () => {
const result = complexFunction();
debugger; // execution pauses here
expect(result).toBe(42);
});
3. VS Code Jest Debugging
Add the following configuration to .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["Debugging Methods"]
LOG["console.log\nSimple & Quick"]
DEBUGGER["debugger\nStep-through"]
VSCODE["VS Code\nBreakpoints"]
end
LOG -->|"Beginner"| Q1["Quick checks"]
DEBUGGER -->|"Intermediate"| Q2["Detailed inspection"]
VSCODE -->|"Advanced"| Q3["Efficient workflow"]
style LOG fill:#22c55e,color:#fff
style DEBUGGER fill:#f59e0b,color:#fff
style VSCODE fill:#3b82f6,color:#fff
Useful CLI Options
Running a Single Test File
# 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"
The --verbose Flag
Displays detailed test results, showing each test name and its outcome individually.
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)
The --bail Flag
Stops test execution after the first failure. Useful for saving time in CI environments.
# stop after first failure
npx jest --bail
# stop after N failures
npx jest --bail=3
Watch Mode
Automatically re-runs tests when files change.
npx jest --watch # changed files only
npx jest --watchAll # all tests
Other Useful Flags
| Flag | Description |
|---|---|
--coverage |
Generate coverage report |
--verbose |
Show detailed test results |
--bail |
Stop on first failure |
--watch |
Re-run tests for changed files |
--watchAll |
Re-run all tests on change |
--runInBand |
Run tests serially (useful for debugging) |
--no-cache |
Run without cache |
--clearCache |
Clear Jest's cache |
--detectOpenHandles |
Detect handles preventing exit |
--forceExit |
Force exit after tests complete |
Useful jest.config.js Options
// 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 version:
// 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;
| Option | Description | Default |
|---|---|---|
testMatch |
Test file glob patterns | ['**/__tests__/**/*.[jt]s?(x)', '**/?(*.)+(spec|test).[jt]s?(x)'] |
testPathPattern |
Regex filter for test paths | (none) |
testTimeout |
Timeout per test in ms | 5000 |
clearMocks |
Auto-clear mocks between tests | false |
verbose |
Show detailed results | false |
bail |
Stop on failure | 0 (no bail) |
maxWorkers |
Number of parallel workers | Half of CPU cores |
Troubleshooting Common Jest Errors
1. "Cannot find module" Error
Cannot find module './utils' from 'src/app.test.js'
Cause and Solution:
// check file path and extension
// jest.config.js
module.exports = {
moduleFileExtensions: ['js', 'ts', 'json'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
},
};
2. "SyntaxError: Unexpected token" Error
SyntaxError: Unexpected token 'export'
Cause: ESM syntax is not being transpiled.
// jest.config.js
module.exports = {
transform: {
'^.+\\.jsx?$': 'babel-jest',
'^.+\\.tsx?$': 'ts-jest',
},
transformIgnorePatterns: [
'/node_modules/(?!(some-esm-package)/)',
],
};
3. "Async callback was not invoked" Error
Timeout - Async callback was not invoked within the 5000 ms timeout
Cause: An async operation did not complete in time.
// 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. Tests Won't Exit
Jest did not exit one second after the test run has completed.
Cause: Open handles (database connections, timers, etc.)
# detect open handles
npx jest --detectOpenHandles
# force exit (last resort)
npx jest --forceExit
// proper cleanup
afterAll(async () => {
await db.close();
await server.close();
});
5. State Leaking Between Tests
// 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
});
Troubleshooting Checklist
flowchart TB
START["Test Failed"] --> Q1{"What is the\nerror message?"}
Q1 -->|"Module not found"| A1["Check paths and\nmoduleNameMapper"]
Q1 -->|"Syntax Error"| A2["Check transform and\nbabel config"]
Q1 -->|"Timeout"| A3["Consider increasing\ntestTimeout"]
Q1 -->|"Won't exit"| A4["Use --detectOpenHandles\nto find the cause"]
Q1 -->|"Assertion failed"| A5["Use console.log\nto inspect values"]
A1 --> FIX["Fix and re-run"]
A2 --> FIX
A3 --> FIX
A4 --> FIX
A5 --> FIX
style START fill:#ef4444,color:#fff
style FIX fill:#22c55e,color:#fff
Summary
| Concept | Description |
|---|---|
| Statement Coverage | Percentage of executed statements |
| Branch Coverage | Percentage of decision paths taken |
| Function Coverage | Percentage of called functions |
| Line Coverage | Percentage of executed lines |
--coverage |
CLI flag to generate coverage reports |
coverageThreshold |
Enforce minimum coverage levels |
--verbose |
Show detailed test results |
--bail |
Stop on first failure |
--runInBand |
Run tests serially (for debugging) |
--detectOpenHandles |
Detect handles preventing exit |
Key Takeaways
- Coverage is a tool for finding untested code, not a measure of test quality
- Set coverage thresholds and use them as quality gates in CI/CD
- Start debugging with
console.log, then escalate todebuggeror VS Code as needed - Combine
--bail,--verbose, and--runInBandfor efficient debugging - Learn common error patterns so you can resolve them quickly
Exercises
Exercise 1: Basics
Write tests to achieve 100% coverage for the following function.
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';
}
Exercise 2: Applied
Complete the following jest.config.js with these requirements:
- Coverage threshold of 80% for all metrics
- Collect coverage only from
src/ - Exclude
.d.tsfiles - Test timeout of 10 seconds
// jest.config.js
module.exports = {
// your config here
};
Challenge
Write test cases that achieve 50% branch coverage and then 100% branch coverage for the following code.
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 };
}
References
Next Up: In Day 9, we'll work on a Hands-on Project. You'll put everything you've learned into practice by writing tests for a real application!