In this article, we will look at the best practices for optimizing unit testing in JavaScript. I’ll discuss some of the best tools for unit testing such as Jest, jest-circus, and React Testing Library (RTL). Moreso we’ll discuss how you can configure these tools for optimal performance. I’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.
I prefer to use the following tools for unit testing:
jest-circus
is a test runner built on top of Jest. It provides parallelized execution capabilities for tests. Starting with Jest v27, it comes as the default test runner in jest 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-offline
library. 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.
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.
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 the 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.
The --bail
command is useful when you need to do a quick sanity check to see whether all tests are still passing - run tests until the first fails. If a fail occurs, use a more suitable command for continuous troubleshooting and fixing of 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 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.
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.
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 a 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 the storyshot
add-on to automatically cover all components in your UI library with snapshot tests.
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. Please benchmark it if you can - from my previous experience, "it depends". I guess I was lucky enough to have powerful CI machines.
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.
Originally published here.
You can also follow me on Twitter and connect on LinkedIn to get notifications about new posts!