You’re running Jest tests and want to make sure your code coverage never dips below a certain percentage, failing your CI build if it does.

This is usually because the jest-junit reporter, which is often used to generate coverage reports for CI systems, is not configured to exit with an error code when coverage thresholds are not met. Instead, it just generates the report, and the CI job continues, potentially with insufficient coverage.

Here are the common reasons this happens and how to fix them:

1. coverageThreshold Not Configured in jest.config.js

Jest has a built-in mechanism for setting coverage thresholds. If this section is missing or incorrectly configured, Jest won’t know what your targets are.

Diagnosis: Check your jest.config.js (or jest.config.ts) file for a coverageThreshold property.

Fix: Add or update the coverageThreshold property in your jest.config.js:

// jest.config.js
module.exports = {
  // ... other Jest configurations
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80,
    },
    // You can also set thresholds for specific files or directories
    // './src/components/': {
    //   branches: 90,
    //   functions: 90,
    //   lines: 90,
    //   statements: 90,
    // },
  },
};

Why it works: This tells Jest the minimum percentage of branches, functions, lines, and statements that must be covered by your tests. Jest will compare the actual coverage against these thresholds.

2. jest-junit Reporter Not Configured to Fail on Thresholds

The jest-junit reporter is popular for generating JUnit XML reports, which many CI systems parse. By default, it doesn’t necessarily fail the build if coverage thresholds are missed.

Diagnosis: Look at your jest.config.js for the reporters array and check the jest-junit configuration.

Fix: Ensure jest-junit is configured with allowExternalErrors set to true and suiteNameTemplate that includes coverage information. More importantly, you need to ensure Jest itself is exiting with a non-zero code when thresholds are missed. The jest-junit reporter itself doesn’t fail the build; it just reports the results. The failure comes from Jest’s exit code.

A more direct approach is to use the coverageReporters option in Jest and ensure a reporter that checks thresholds is present. The jest-junit reporter is primarily for reporting results, not enforcing them.

Correct Fix (using Jest’s built-in enforcement): Ensure your jest.config.js has collectCoverage: true and coverageThreshold configured (as in point 1). When Jest runs with these options and the coverage falls below the threshold, it will automatically exit with a non-zero status code, which your CI will interpret as a failure.

If you are also using jest-junit for reporting, ensure it’s configured correctly alongside Jest’s own threshold checking. A common setup is:

// jest.config.js
module.exports = {
  // ... other Jest configurations
  collectCoverage: true,
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80,
    },
  },
  reporters: [
    'default',
    [ 'jest-junit', {
      outputDirectory: 'reports/junit',
      outputName: 'junit.xml',
      suiteNameTemplate: ({ testPath, config }) => {
        // This template is less critical for failure, more for reporting organization
        return config.testEnvironment;
      },
      // The key is that Jest itself fails if thresholds are not met.
      // jest-junit just reports the outcome.
    }]
  ],
  // You might also use coverageReporters for other formats
  // coverageReporters: ['json', 'lcov', 'text', 'clover', 'jest-junit'],
};

Why it works: When collectCoverage is true and coverageThreshold is set, Jest will perform the threshold check before exiting. If the thresholds are not met, Jest’s process will exit with a non-zero status code (typically 1), signaling an error. CI systems are designed to fail when a process exits with a non-zero code.

3. Incorrect collectCoverage Setting

Jest needs to be explicitly told to collect coverage.

Diagnosis: Check your jest.config.js for collectCoverage.

Fix: Ensure collectCoverage is set to true:

// jest.config.js
module.exports = {
  // ... other Jest configurations
  collectCoverage: true,
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80,
    },
  },
};

Why it works: This flag enables Jest’s coverage collection mechanism. Without it, Jest will run tests but won’t generate any coverage data to check against thresholds.

4. Thresholds Set Too High or Tests Not Comprehensive Enough

You might have configured thresholds correctly, but your actual coverage is genuinely lower than desired, and your tests aren’t covering enough code.

Diagnosis: Run Jest locally with the coverage command (npm test -- --coverage or yarn test --coverage). Examine the output and the generated coverage/lcov-report/index.html (or similar) to see which files and lines have low coverage.

Fix: Increase the test coverage by writing more tests, especially for edge cases, unexercised logic, and new features. Alternatively, if the current thresholds are too aggressive for your project’s stage, you can lower them temporarily.

Example: If your coverage is 75% and your threshold is 80%, you need to write tests that cover more code. If you decide 80% is too high right now, you could change lines: 80 to lines: 75 in coverageThreshold.

Why it works: This directly addresses the root cause: insufficient code coverage. Either by improving the code being tested or adjusting the target, you align the reality with the requirement.

5. CI Environment Not Properly Detecting Jest’s Exit Code

Your CI pipeline might be configured to ignore non-zero exit codes from certain commands, or it might not be running the Jest command in a way that properly surfaces its exit status.

Diagnosis: Examine your CI configuration file (e.g., .github/workflows/main.yml for GitHub Actions, Jenkinsfile for Jenkins, .gitlab-ci.yml for GitLab CI). Look for commands that execute Jest and how their success/failure is determined.

Fix (GitHub Actions Example): Ensure the step that runs Jest is not configured to continue-on-error: true and that the CI job itself doesn’t have a global setting to ignore failures.

# .github/workflows/main.yml
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v3

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'

      - name: Install dependencies
        run: npm ci

      - name: Run tests with coverage
        run: npm test -- --coverage
        # No 'continue-on-error: true' here!

Why it works: CI systems by default treat any command exiting with a non-zero status code as a failure. If your CI is configured to ignore these failures (e.g., continue-on-error: true), it won’t stop the build. Removing this setting ensures the CI correctly interprets Jest’s failure signal.

6. jest-junit Configuration Causing Test Runner to Not Exit Properly (Less Common)

In very specific jest-junit configurations, especially if you’re trying to use it to force a failure and not relying on Jest’s native exit code, you might encounter issues. However, the standard practice is to let Jest handle the exit code.

Diagnosis: Review the jest-junit plugin documentation for any specific options related to error handling or exit codes. Ensure you’re not trying to override Jest’s core behavior in a way that breaks it.

Fix: Stick to Jest’s coverageThreshold for failing the build. Use jest-junit primarily for generating reports that CI systems can parse for display.

For example, if you have something like this in your jest-junit config that tries to force a failure based on coverage data within the reporter itself, it might interfere:

// THIS IS LIKELY UNNECESSARY AND POTENTIALLY PROBLEMATIC
// IF YOU ARE RELYING ON JEST'S NATIVE EXIT CODE
[ 'jest-junit', {
  // ... other options
  // avoid any custom logic here that tries to force a non-zero exit code
  // based on coverage, let Jest do it.
}]

Why it works: By simplifying jest-junit to its reporting role and relying on Jest’s built-in coverageThreshold mechanism for failure, you ensure a more robust and standard setup. Jest’s exit code is the universal signal for build failure.

After fixing these, the next error you’ll likely see is a specific file or module having coverage below your set threshold, with Jest clearly indicating which part failed.

Want structured learning?

Take the full Jest course →