Jest - Speed Up Slow Test Suites

Cover Image for Jest - Speed Up Slow Test Suites

Speeding Up Slow CI Jest Tests

Large and complex Jest test suites, especially those leveraging jsdom, can bring containers and continuous integrations environments to a halt.

Fortunately, those who searched for solutions to this problem before us opened issues on the Jest project. Now we have a cli option that dramatically reduces test suite execution time, --runInBand.

Jest's --runInBand Option

Start with the --runInBand when debugging slow Jest tests in your continuous integration environment.

As cited in the docs, it runs the tests serially rather than creating a bunch of workers. Depending on your CI environment's available resources, this can dramatically speed up the test suite.

GitHub --runInBand testimony

While your milage will vary, it's fair to say some have seen huge improvements by using --runInBand. On the off chance --runInBand leaves your test suite running slow, maybe running more workers will do the trick - Give the --maxWorkers options a try!

Jest's --maxWorkers Option

--maxWorkers specifies the number of workers that spawn in a pool when the test suite executes.

Unlike --runInBand which limits the size of the pool to 1 worker, --maxWorkers allows you to specify the number of workers you want in the pool.

When running a Jest test suite locally, the --maxWorkers value defaults to the number CPUs on your machine minus 1, the subtracted CPU accounting for the main thread.

When running Jest with a watch argument, it defaults to half the machine's CPU. This prevents Jest from eating up the available processing power, making it possible to run the test suite and develop simultaneously.

Although the defaults work well in most cases, others may benefit from adjusting the --maxWorker count. This is particularly true in continuous integration environments, where doubling the --maxWorker count may cut test execution time in half.

TIP - If you're experimenting with this argument in a continuous integration environment, consider opening a branch for each worker count value you want to test. Then see which (if any) branch runs the fastest in the CI environment before merging it into your master or main development branch.

--runInBand & --maxWorkers Jest Source Code

Curious how these arguments work and want to dive in a bit deeper? Check out the getMaxWorkers() function from the Jest source.

export default function getMaxWorkers(argv: Argv): number {
  if (argv.runInBand) {
    return 1;
  } else if (argv.maxWorkers) {
    return parseInt(argv.maxWorkers, 10);
  } else {
    const cpus = os.cpus().length;
    return Math.max(argv.watch ? Math.floor(cpus / 2) : cpus - 1, 1);
  }
}

As visible in the above example, --maxWorkers=1 is equivalent to --runInBand.

If --maxWorkers and --runInBand fail to trim down your CI test suite run time, check out Jest's troubleshooting docs for more info on how to improve test run times in resource constrained environments.


Make Local Jest Tests Faster

Local Jest Gotcha

Running Jest with the correct options for the execution environment makes a big difference in the time it takes for the test suite to run.

These findings won't come as a revelation to those familiar with Jest. But, if you've read this far you're likely stilling looking for ways to speed up your tests. I walk through the following anecdotal example as a reminder that simple configuration issues can be costly.

Keep the Cache

tl;dr If you're trying to cut down your local test execution time, make sure you're using Jest's cache

I recently jumped into an existing React application, working to extend existing functionality while authoring a few new components. I used the an existing "test" script in the package.json without reviewing the script's Jest options.

Despite the test suite containing less than 100 tests, it was taking over 40 seconds each time it ran. After spending the better part of an hour on Jest test execution, this annoyance turned into a major pain point in my test-driven-development process.

I was motivated to action, and fortunately, the fix was simple.

{
  "scripts": {
    "test": "jest --no-cache ..."
  }
}

Unsurprisingly, disabling the Jest cache causes the local test execution time to balloon. This seemingly innocuous Jest likely slipped into the code base when another developer was debugging the test environment, locally or in the continuous integration environment.

I removed the hardcoded --no-cache option from the project's package.json "test" script.

This simple investigation trimmed the local test execution time - the time dropped from ~40 to ~12 seconds. This is to be expected according to the Jest documentation,

Note: the cache should only be disabled if you are experiencing caching related problems. On average, disabling the cache makes Jest at least two times slower.

The ROI of Improving the Developer Experience

While that may not seem dramatic at first glance, spending a few minutes to improve the project's local test execution efficiency yields long-term business value.

Let's assume a developer working on the project runs the test suite 10 times per hour, and they're now saving 30 seconds per test suite execution. This simple change saves about 5 minutes per hour.

These developer time savings look impressive annually, and can compound when the time is well invested. Spending a few minutes to debug my slow test suite will benefits all current and future developers on the project.

For example, if I spend 20 hours a week on the above project for a year, the annual ROI is over 80 hours of developer time saved 💫