Skip to content

Software Testing & QA Tools

Jest

Jest is an all-in-one JavaScript and TypeScript testing tool: a test runner, an assertion library (expect), and mocking utilities shipped as a single dependency, with near-zero configuration for most projects. It was built at Facebook to test React and moved to the OpenJS Foundation in 2022, and it's now used as widely for plain Node.js backends and libraries as for React itself. Its most distinctive feature is snapshot testing — capture a rendered output or object shape once, then fail any future run that produces something different — layered on top of isolated, parallelized test files.

Why it matters

It's the assumed default across the React and Node ecosystem
Create React App's legacy setup, most React tutorials, and a large share of existing Node repos wire up Jest without asking, so describe/it/expect is often a developer's first exposure to JS testing whether they picked it or not.
One dependency replaces three separate decisions
Choosing Jest means you don't have to separately pick a runner, an assertion style, and a mocking library — but it also means you inherit Jest's specific mocking model (jest.fn, jest.mock, jest.spyOn) rather than composing your own.
Snapshot testing catches unintended output changes automatically
A snapshot test serializes a component tree or object on first run and diffs every later run against it, which is good at flagging accidental changes to large rendered output but only useful if a human actually reads the diff before approving it.
Test files run in parallel worker processes by default
Each test file gets its own sandboxed process, so a large suite's wall-clock time in CI scales with available cores rather than running everything serially the way a single-process runner would.

What ships in the box

A Jest install gives you the runner (test/describe/it as globals), the expect assertion API with a large set of matchers (toBe, toEqual, toHaveBeenCalledWith, toMatchSnapshot, and more), mocking (jest.fn, jest.mock, jest.spyOn), and code coverage via --coverage using Istanbul under the hood. Nothing else needs installing to write a working unit test, which is the main reason it became the default for projects that don't want to make three separate library decisions up front.

JavaScript
// sum.js
function sum(a, b) {
  return a + b;
}
module.exports = sum;

// sum.test.js
const sum = require('./sum');

test('adds 1 + 2 to equal 3', () => {
  expect(sum(1, 2)).toBe(3);
});

test('calls a mock exactly once', () => {
  const onComplete = jest.fn();
  onComplete();
  expect(onComplete).toHaveBeenCalledTimes(1);
});

Mocking without a separate library

jest.fn() creates a standalone mock function that records every call and its arguments. jest.mock('./module') auto-mocks an entire module's exports, and Babel hoists that call to the top of the file — above your imports — so it takes effect before anything else runs. jest.spyOn(obj, 'method') wraps an existing method while still tracking calls, and can restore the original implementation afterward. The hoisting behavior is convenient once you know about it and confusing the first time you don't, since a jest.mock() factory that references an outer-scope variable throws a reference error that looks like it shouldn't be possible given the code's visual order.

Snapshot testing, and when it stops being useful

A snapshot test is good at catching drift across large, hard-to-hand-write outputs — a rendered component tree, a big API response object — because it doesn't require writing an assertion for every field. It's bad at explaining why a change matters: when a snapshot fails, the default reflex under deadline pressure is to run jest -u and move on, which quietly turns the test from 'this output shouldn't change unexpectedly' into 'this output is whatever the code currently produces.' Snapshots are worth the most on stable, rarely-changing output and the least on anything that legitimately changes often.

Async tests and fake timers

A test can return a promise, use async/await, or (in older code) accept a done callback that must be called for the test to complete — forgetting to return or await a promise means Jest can finish the test before a rejected assertion inside it is ever observed. jest.useFakeTimers() replaces the real setTimeout/setInterval/Date with controllable fakes, letting a test advance simulated time (jest.advanceTimersByTime) instead of actually waiting, which is how you test a five-second debounce without a five-second test.

Mistakes people make here

Running jest -u to fix a red snapshot test without reading the diff
This turns a regression check into a rubber stamp — the snapshot becomes a record of whatever the code currently does rather than a check against what it's supposed to do, so a real bug that changed the output gets accepted as the new baseline.
Not resetting mocks between tests
A jest.fn()'s call history and mock implementation persist across every test in the same file unless you call jest.clearAllMocks() or jest.resetAllMocks() (or set clearMocks/resetMocks in config), so one test's mock calls can silently leak into another test's assertions on 'was called once'.
Assuming the test environment includes DOM globals like document and window
Since Jest 27 the default testEnvironment is 'node', not 'jsdom' — a test written against document fails with a reference error until testEnvironment: 'jsdom' is set explicitly, and as of Jest 28 that jsdom environment isn't even bundled by default; it needs the separate jest-environment-jsdom package installed.
Treating jest.mock() as an ordinary function call that runs where it's written
Babel hoists jest.mock() calls to the top of the file, before your imports execute, specifically so the mock is in place before the real module loads — but that means a mock factory referencing a variable declared later in the file (via closure) can throw or silently see undefined, because the hoisted call runs before that variable is initialized.

Strengths and trade-offs

Where it is strong

  • Minimal setup: works with effectively zero configuration on most Node and React projects, inferring what to run from file naming conventions.
  • One dependency covers running, asserting, and mocking, avoiding the version-matching and wiring needed when composing separate libraries.
  • Parallelized, isolated test files by default make large suites fast in CI without extra configuration.
  • Deep tooling integration — IDE test runners, coverage reporting, and watch mode with smart re-runs based on changed files — comes built in.

The trade-offs

  • Its all-in-one design is heavier and more opinionated than composing a bare runner with only the assertion and mocking libraries you actually want.
  • Module mocking's hoisting and auto-mock behavior is a recurring source of confusing failures for developers new to it.
  • For plain Node/TypeScript projects, newer runners built on Vite/esbuild (notably Vitest, which mirrors Jest's API) are often faster since they skip Jest's Babel or ts-jest transform step.
  • jsdom, the default DOM shim used for React component tests, doesn't behave identically to a real browser, so timing- or rendering-sensitive bugs sometimes need a real-browser tool like Playwright to catch.

Who needs this

Any team writing automated tests for a React (or similar) frontend or a Node.js backend, since Jest is the default most generators, tutorials, and existing codebases already assume — even developers who'd choose differently on a blank project will run into it.

Questions about jest

Is Jest only useful for testing React components?
No — Jest's runner, assertions, and mocking work on any JavaScript or TypeScript code, including plain Node.js services and libraries with no UI at all. Its React association comes from its origin and from tools like React Testing Library being commonly paired with it, not from any hard dependency on React.
What's the difference between jest.fn, jest.spyOn, and jest.mock?
jest.fn() creates a brand-new mock function from nothing. jest.spyOn(obj, 'method') wraps an existing method so you can track or override calls while optionally keeping the real implementation. jest.mock('./path') replaces an entire module's exports with mocks, hoisted above your imports so it applies before the real module loads.
Why does my test fail with 'document is not defined'?
Since Jest 27, the default test environment is 'node', which has no DOM. Set testEnvironment: 'jsdom' in your Jest config (and, from Jest 28 onward, install the separate jest-environment-jsdom package) for tests that touch document, window, or other browser globals.
Is Jest still actively maintained given newer options like Vitest exist?
Yes — it moved from Meta to the OpenJS Foundation in 2022 and continues to be developed and widely used, including at companies with large existing suites. Vitest has become the more common choice for greenfield Vite-based projects specifically because of its speed, but Jest remains the safer default for Create React App-style or plain Node projects.

The primary source

Related concepts

← All concept guides