Learn Jest in 10 DaysDay 8: Coverage and Debugging
Chapter 8Learn Jest in 10 Days

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)
  • xN next 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.log output can get buried. Combine it with the --verbose flag 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

  1. Coverage is a tool for finding untested code, not a measure of test quality
  2. Set coverage thresholds and use them as quality gates in CI/CD
  3. Start debugging with console.log, then escalate to debugger or VS Code as needed
  4. Combine --bail, --verbose, and --runInBand for efficient debugging
  5. 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.ts files
  • 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!