Test Runner

Jest

Jest is the classic JavaScript test runner: batteries-included, snapshot-capable and still the default in countless Node and React codebases.

intermediate13 min readUpdated Sep 15, 2026
sum.test.js
js
// sum.test.js
import { sum } from "./sum";

describe("sum", () => {
  test("adds two numbers", () => {
    expect(sum(2, 3)).toBe(5);
  });

  test("handles negatives", () => {
    expect(sum(-1, -1)).toBe(-2);
  });
});
Maintained by
Meta
API
describe, test, expect
Mocks
jest.fn, jest.mock
Snapshots
Built in
Coverage
Built in
Transforms
Babel, ts-jest, SWC

Why it matters

Why Jest still matters

Batteries included

Assertions, mocking, snapshots and coverage ship together, so a new project needs very little setup.

Snapshots

Capture output once and detect unintended changes, useful for large serialisable structures.

Mature ecosystem

Years of plugins, presets and integrations mean almost any stack has a documented Jest setup.

The big picture

The three ideas behind Jest

A zero-config runner, a familiar assertion API and built-in mocking, snapshots and coverage.

The runner

Execute

Discovers test files, runs them in isolated environments and reports results.

Assertions and mocks

Verify

expect with matchers, plus jest.fn, jest.spyOn and jest.mock for isolation.

Transform pipeline

Build

Babel, ts-jest or SWC compile modern syntax and JSX before tests run.

Jest at a glance

The core of Jest

describe and test

Group suites and declare individual cases by behaviour.

Matchers

toBe, toEqual, toMatchObject, toThrow and many more.

Mocks and spies

jest.fn, jest.spyOn and jest.mock isolate the unit under test.

Fake timers

Control setTimeout and Date for deterministic timing tests.

Snapshots

Store a serialised result and compare it on later runs.

Coverage

Built-in coverage reporting with thresholds.

A short history

The runner that defined a generation

  1. 2014

    Jest released

    Facebook introduces a test runner designed for React and JavaScript projects.

    14
  2. 2016

    Jest 15 and 16

    A rewrite drops configuration friction and snapshot testing becomes mainstream.

    16
  3. 2018

    Jest 23 and beyond

    Jest becomes the most widely used JavaScript test runner.

    18
  4. 2021

    Vite and Vitest rise

    Vitest offers a faster, Vite-native alternative with a compatible API.

    21
  5. Today

    Still everywhere

    Dominant in existing Node, React and React Native codebases, and fully maintained.

    Today

The complete guide

Jest: Everything you need to know

What is Jest?

Jest is a JavaScript test runner created at Facebook and now one of the most widely used testing tools in the ecosystem. It is batteries-included: assertions, mocking, snapshot testing and coverage all ship in one package, and a new project needs very little configuration to get started.

For years Jest was the default choice for React and Node projects, and it remains dominant in existing codebases. Even with Vitest rising, Jest’s API is the shared vocabulary of JavaScript testing, which makes it worth knowing regardless of which runner you use day to day.

Writing tests

A Jest test file uses describe to group and test (or it) to declare a case.

// cart.test.js
import { Cart } from "./cart";

describe("Cart", () => {
  test("starts empty", () => {
    expect(new Cart().items).toEqual([]);
  });

  test("adds an item", () => {
    const cart = new Cart();
    cart.add({ id: 1, price: 10 });
    expect(cart.items).toHaveLength(1);
  });
});

Use beforeEach and afterEach for setup and teardown, and beforeAll and afterAll when a resource should be created once for the whole file. Name tests after behaviour so a failure tells you what broke.

Matchers

Jest’s expect API is large and expressive.

// matchers.test.js
expect(value).toBe(5);                 // strict equality
expect(value).toEqual({ a: 1 });       // deep equality
expect(value).toBeDefined();
expect(list).toContain("a");
expect(fn).toThrow("invalid");
expect(value).toBeGreaterThan(3);
expect(obj).toMatchObject({ id: 1 });  // partial match

toBe compares with Object.is and is right for primitives. toEqual recursively compares objects and arrays. toStrictEqual is stricter about types and undefined properties. toMatchObject is useful when you only care about a subset of a structure.

Mocks, spies and module mocking

Jest’s mocking is one of its strongest features.

// users.test.js
import { jest } from "@jest/globals";
import { getUser } from "./users";

jest.mock("./http", () => ({
  get: jest.fn().mockResolvedValue({ id: 1, name: "Ada" }),
}));

test("returns a user", async () => {
  await expect(getUser(1)).resolves.toEqual({ id: 1, name: "Ada" });
});

jest.fn creates a mock function, jest.spyOn wraps a real method so you can observe calls and restore it later, and jest.mock replaces a module. Because jest.mock is hoisted above imports, the mock is in place before the module under test loads.

Always reset mocks between tests. Enable clearMocks, resetMocks or restoreMocks in the config, or call the matching ...AllMocks helper in afterEach. Leaked mock state is one of the most common reasons tests pass alone and fail together.

Async testing

Use async/await with resolves and rejects, exactly as you would in Vitest.

// async.test.js
test("rejects on failure", async () => {
  await expect(loadUser(-1)).rejects.toThrow("Invalid id");
});

The done callback works but is easy to misuse. Prefer awaiting. For timers, jest.useFakeTimers() plus jest.advanceTimersByTime() makes debounce and retry tests deterministic.

Snapshots

Snapshots serialise a value and compare it on future runs.

// config.test.js
test("builds the default config", () => {
  expect(createConfig({ debug: true })).toMatchInlineSnapshot();
});

Inline snapshots live in the test file, which makes them reviewable in a diff. External snapshots are convenient for large output but are frequently updated without inspection, which turns them into a rubber stamp. Use snapshots for stable serialisable data, not as a substitute for thinking about what matters.

Configuration

Jest reads a config from jest.config.js, a jest key in package.json or a CLI flag.

// jest.config.js
export default {
  testEnvironment: "jsdom",
  clearMocks: true,
  collectCoverage: true,
  coverageThreshold: {
    global: { lines: 80, functions: 80 },
  },
  transform: {
    "^.+\\.(t|j)sx?$": ["@swc/jest"],
  },
};

testEnvironment selects node or jsdom. clearMocks prevents leakage. Coverage thresholds turn a goal into an enforced rule. The transform decides how TypeScript and JSX are compiled, with ts-jest, Babel and SWC as the common options.

Where Jest fits

Jest is the unit and integration layer. For component behaviour, pair it with Testing Library to test through the user’s perspective. For full user flows, use an end-to-end tool such as Playwright or Cypress. If you are starting a new Vite project, Vitest offers the same API with a faster, shared pipeline.

Best practices

  • Test behaviour through public APIs, not private internals.
  • Keep one logical assertion per test where practical.
  • Reset and restore mocks between tests.
  • Prefer async/await over done.
  • Use fake timers for anything time-dependent.
  • Snapshot small, stable structures and review every update.
  • Run tests in CI on every change, not only locally.

Common mistakes

  • Asserting on implementation details and breaking tests during refactors.
  • Blindly updating snapshots.
  • Forgetting to clear mocks and leaking state across tests.
  • Using real timers and making tests slow or flaky.
  • Testing everything at the unit level and missing integration bugs.
  • Ignoring coverage while assuming tests cover the important paths.

Where to go next

Jest is a dependable foundation and a shared vocabulary across the ecosystem. Compare it with Vitest for modern Vite projects, add Testing Library for components, and cover user journeys with Playwright or Cypress. Then write a test for a bug you recently fixed and let it protect you from regressions.

Testing async code

Await the promise and assert on the outcome. The done callback is easy to forget and causes confusing timeouts.

Prefer
test("loads the user", async () => {
  await expect(getUser(1)).resolves.toEqual({
    id: 1,
    name: "Ada",
  });
});
Avoid
test("loads the user", (done) => {
  getUser(1).then((user) => {
    expect(user.name).toBe("Ada");
    done();
  });
});

Snapshot scope

Snapshot small, stable structures. A huge snapshot that everyone blindly updates is worse than no test.

Prefer
expect(parseConfig(input)).toMatchInlineSnapshot(`
  {
    "debug": true,
    "level": "info",
  }
`);
Avoid
expect(renderWholeApp()).toMatchSnapshot();
// huge, unstable and always
// updated without review

FAQ

Frequently asked questions

Keep learning

Related topics from the roadmap.

$ start learning

Ready to start learning Jest?

Our interactive tutorial walks you through Jest step by step — with quizzes and real code you can run in the browser.