Skip to content
Node.js nd testing 5 min read

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.

MatcherPurpose
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 / rejectsUnwrap 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 beforeEach over beforeAll for 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 beforeEach rather than relying on test execution order.
  • Prefer specific matchers (toEqual, toThrow, toContain) over toBeTruthy for clearer failure output.
  • Always await async assertions, or use resolves/rejects, to avoid silent false passes.
  • Run with --coverage in CI and set a coverageThreshold to prevent regressions.
  • For new ESM-first projects, evaluate Vitest or the Node test runner before committing to Jest’s experimental ESM path.
Last updated June 14, 2026
Was this helpful?