Skip to content

Optimizing Unit Testing in JavaScript: Tips, Tools, and Best Practices

In this article, we will focus on optimizing unit testing in JavaScript. We will discuss some of the best tools for unit testing such as Jest, jest-circus, and React Testing Library (RTL) and show you how to configure these tools for optimal performance. We’ll also provide tips and best practices for improving your testing workflow, so you can ensure that your code is reliable and working as expected. Whether you’re a seasoned developer or just getting started with unit testing, this guide will help you improve your testing process and produce high-quality code.

Unit tests

I prefer to use the following tools for unit testing:

  • Jest. It’s a JavaScript testing framework that can be easily integrated with any other testing and development tool.
  • jest-circus test runner built on top of Jest and providing parallelized execution capabilities for tests. Starting with Jest v27, it comes as the default test runner in jest configuration.
  • React Testing Library. It’s a go-to tool for testing React components. I used to have Enzyme on this list a few years ago but unfortunately, it’s not maintained anymore and I highly don’t recommend using it in your project. You can read about the strategy and tools I use to migrate projects from Enzyme to RTL in my article.

Configuration

React Testing Library setup

JSDOM’s implementation of getComputedStyle() is super slow. As I do not usually do any style-specific assertions and validations in my unit tests, I prefer to mock this API to speed up RTL tests by up to 2x.

// somewhere in your setup file
global.getComputedStyle = () => {
  return {
    getPropertyValue: () => {
      return undefined;
    },
  };
};

Alternatively, you can switch from JSDOM implementation to LightDOM. You can try both approaches together as well.

No network calls

Network calls in unit tests might significantly slow down test performance. There is an easy solution for this: jest-offlinelibrary. It will fast-fail all tests that attempt to access the network. If you are using fetch API in your project then jest-fetch-mock will automatically mock all API requests for you:

import { enableFetchMocks } from 'jest-fetch-mock';

enableFetchMocks();

But some third-party libraries might use different mechanisms to integrate with the network API (good old XHR requests, for instance). Therefore, I prefer to use these 2 tools together – they perfectly complement each other.

Transpiler

If you are using babel-jest or ts-jest in your Jest configuration to transpile JavaScript/TypeScript files, then I have bad news for you – they will slow down your tests too! Nowadays, cool kids use swc/jest or esbuild transpilers that are written by faster Rust and Go languages respectively. It’s easy to switch – highly recommend you do it right away. Really a low-hanging fruit for improving your test performance.

Tools

Use jest-slow-test-reporter to identify what tests are the slowest! Usually, you will get a significant performance boost by addressing just a small portion of your tests. My rule of thumb is unit test execution time should not exceed 300ms.

Local development

Commands:

    "test": "TZ=America/Los_Angeles LANG=en_US.UTF-8 jest --config jest.config.json --passWithNoTests --maxWorkers=50%",
    "test:bail": "npm test -- --bail",
    "test:diff": "npm test -- -o --watch",
    "test:clean": "jest --clearCache"

Hints:

TZ=America/Los_Angeles LANG=en_US.UTF-8 environment variables are needed to lock dates and timezones – crucial when you have Node v14+ and use date-fns-tz in your code.

--maxWorkers=50% usually help to improve test performance by 20% but requires careful benchmarking. Inspired by this article.

I prefer to use JSON file (jest.config.json) for configuration rather than .js or .ts. Reason:  jest is requesting ts-node from the transpiling of jest.config.ts. For TypeScript file improvement usually 2X, for JS files it’s pretty much the same as JSON.

--bail command is useful when you need to do a quick sanity check whether all tests are still passing – run tests until the first fails. If fails, use a more suitable command for continuous troubleshooting and fixing the tests.

test:diff will run only tests for files staged in your local Git workspace and will do it in watch mode. This is the most popular command in my arsenal that I usually run while I refactor some existing code (for example, re-writing React class component to functional component). With good coverage and well-written test code, it’s super easy to catch regressions.

Submitting code review

If you still don’t use husky and lint-staged as part of Git pre-commit hook integration, then you should. Here is the configuration I usually have:

  "lint-staged": {
    "*.{js,jsx}": [
      "eslint -c ./.eslintrc.js --fix",
      "git add",
      "npm test -- --findRelatedTests"
    ]
  }

npm test -- --findRelatedTests will run only tests related to files that have changed. You can read more about how it works in one of my previous articles.

Storybook

If your project uses StoryBook to run your components in isolation (let’s say you have an internal UI library), then you can also include start-storybook --smoke-test to your pre-commit configuration – it will execute dry-run of the start command and fail immediately if something is wrong (maybe you have some breaking change in the component contract?).

Also, you can use storyshot add-on to automatically cover all components in your UI library with snapshot tests.

Continuous integration (CI)

Command

"test:ci": "npm run test -- --runInBand"

According to this GitHub issue, --runInBand performs better in CI environments rather than local ones due to the resource constraints of such environments. Basically speaking, with runInBand we tell Jest to run tests serially rather than orchestrate a thread pool of test workers (the same effect as using --maxWorkers=1). Please benchmark it if you can – from my previous experience, “it depends”. I guess I was lucky enough to have powerful CI machines. You can also try to apply --maxWorkers=50% as mentioned in one of previous sections.

Conclusion

In conclusion, unit testing is a crucial part of software development that ensures the quality and reliability of your code. By choosing the right tools and configuring them properly, you can improve the performance of your tests and streamline your testing workflow.


Discover more from The Same Tech

Subscribe to get the latest posts to your email.

Published inBest practicesTesting

Be First to Comment

Leave a Reply