Testing with Jest
Jest is the most widely adopted testing framework in the JavaScript ecosystem, originally built at Meta for React but equally at home testing plain Node.js code. It bundles a test runner, an assertion library, mocking, and code coverage into a single dependency, so there is no need to wire several tools together yourself. This page covers installing Jest, the describe/test/expect trio, the matchers you will reach for daily, lifecycle hooks, configuration, coverage reports, and the ES modules caveat that catches most newcomers.
Installation
Install Jest as a development dependency and add a script to run it. Nothing about Jest needs to be global.
npm install --save-dev jest
Add a test script to package.json so npm test invokes the local binary:
{
"scripts": {
"test": "jest"
}
}
Jest discovers any file ending in .test.js or .spec.js, or anything inside a __tests__ directory, so you rarely need to point it at files manually.
Your first test
A test pairs a description with an assertion. The test() function (aliased as it()) declares a single case, and expect() wraps the value you want to check, followed by a matcher that states the expectation.
// math.js
export function add(a, b) {
return a + b;
}
// math.test.js
import { add } from './math.js';
test('adds two numbers', () => {
expect(add(2, 3)).toBe(5);
});
Run it:
npm test
Output:
PASS ./math.test.js
✓ adds two numbers (2 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Time: 0.42 s
Grouping with describe
describe() groups related tests into a block, which keeps output readable and lets you share setup. Blocks can nest as deeply as your code warrants.
import { add } from './math.js';
describe('add()', () => {
test('handles positive numbers', () => {
expect(add(2, 3)).toBe(5);
});
test('handles negative numbers', () => {
expect(add(-2, -3)).toBe(-5);
});
});
Common matchers
Matchers express what you expect. Choosing the right one produces clearer failure messages than a generic equality check. The table below lists the matchers you will use most.
| Matcher | Purpose |
|---|---|
toBe(value) | Strict Object.is equality — primitives and same-reference objects |
toEqual(value) | Deep structural equality for objects and arrays |
toStrictEqual(value) | Like toEqual but also checks undefined keys and types |
toBeTruthy() / toBeFalsy() | Boolean coercion of a value |
toBeNull() / toBeUndefined() | Exact null / undefined checks |
toContain(item) | Membership in an array or substring in a string |
toThrow(error) | A function throws (optionally matching a message) |
toHaveLength(n) | Array or string length |
resolves / rejects | Unwrap a promise before asserting |
Use toEqual for objects — toBe checks reference identity and will fail on two structurally identical objects.
test('object equality', () => {
const user = { name: 'Ada', roles: ['admin'] };
expect(user).toEqual({ name: 'Ada', roles: ['admin'] });
});
test('throwing functions', () => {
expect(() => JSON.parse('{ broken')).toThrow();
});
test('async values', async () => {
await expect(Promise.resolve(42)).resolves.toBe(42);
});
Negate any matcher by inserting .not before it, as in expect(value).not.toBeNull().
Lifecycle hooks
Hooks run setup and teardown around your tests. beforeEach and afterEach run around every test in their scope, while beforeAll and afterAll run once per file or describe block. Use them to create fixtures, reset shared state, or close resources.
let db;
beforeEach(() => {
db = new Map(); // fresh state for every test
db.set('ada', { name: 'Ada Lovelace' });
});
afterEach(() => {
db.clear();
});
test('reads a seeded record', () => {
expect(db.get('ada').name).toBe('Ada Lovelace');
});
test('starts isolated from the previous test', () => {
db.set('grace', { name: 'Grace Hopper' });
expect(db.size).toBe(2);
});
Prefer
beforeEachoverbeforeAllfor mutable state. Resetting before every test keeps cases independent, so one failing test never cascades into spurious failures in the next.
Configuration
For anything beyond defaults, add a jest.config.js file (or a jest key in package.json). Common options are shown below.
// jest.config.js
export default {
testEnvironment: 'node', // 'node' for backend, 'jsdom' for browser-like
collectCoverage: false, // toggle coverage globally
coverageDirectory: 'coverage',
testMatch: ['**/?(*.)+(spec|test).js'],
setupFilesAfterEnv: ['./jest.setup.js'],
verbose: true,
};
Set testEnvironment: 'node' for server code — it skips the heavier DOM simulation and runs faster.
Running with coverage
Pass --coverage to generate a report showing which lines, branches, and functions your tests exercised.
npm test -- --coverage
Output:
----------|---------|----------|---------|---------|
File | % Stmts | % Branch | % Funcs | % Lines |
----------|---------|----------|---------|---------|
All files | 92.31 | 83.33 | 100 | 92.31 |
math.js | 100 | 100 | 100 | 100 |
----------|---------|----------|---------|---------|
A full HTML report is written to the coverage/ directory. You can enforce minimums with a coverageThreshold config block to fail CI when coverage drops below a set percentage.
The ES modules caveat
Jest historically ran on CommonJS, and its handling of native ES modules is still experimental. If your project uses import/export and you have "type": "module" in package.json, Jest must be launched with the experimental VM modules flag:
node --experimental-vm-modules node_modules/jest/bin/jest.js
A common alternative is to let Babel transpile ESM to CommonJS at test time via babel-jest. If you want first-class, zero-config ESM support instead, consider Vitest or the built-in Node test runner, both of which run native ES modules without flags.
Best practices
- Keep each test focused on one behavior so a failure points to a single cause.
- Reset shared state in
beforeEachrather than relying on test execution order. - Prefer specific matchers (
toEqual,toThrow,toContain) overtoBeTruthyfor clearer failure output. - Always
awaitasync assertions, or useresolves/rejects, to avoid silent false passes. - Run with
--coveragein CI and set acoverageThresholdto prevent regressions. - For new ESM-first projects, evaluate Vitest or the Node test runner before committing to Jest’s experimental ESM path.