cheat sheet

jest

Package-level reference for jest on npm — install variants, transformer ecosystem, peer-dep landscape, vitest competition, and migration paths.

jest

What it is

jest is the long-reigning all-in-one JavaScript test framework: zero-config test runner, assertion library, mocking, snapshot testing, and coverage rolled into one binary. Originally extracted from Facebook's internal test tooling in 2014, it became the de-facto choice for React, Next.js, and Node.js apps for the better part of a decade.

It still ships the most batteries-included DX in the runner space: jest.fn(), jest.mock(), automatic global injection, snapshot files, watch mode with intelligent prompts, and a vast plugin ecosystem (ts-jest, babel-jest, jest-puppeteer, @swc/jest).

Install

bash
# npm / pnpm / yarn / bun
npm install -D jest
pnpm add -D jest
yarn add -D jest
bun add -d jest

Output: jest binary on PATH under node_modules/.bin/jest.

bash
# Common transformer / type add-ons
npm install -D @types/jest               # types for jest globals
npm install -D ts-jest                   # TypeScript transformer (legacy default)
npm install -D @swc/jest                 # SWC-backed transformer (10-20x faster)
npm install -D babel-jest @babel/preset-env  # Babel transformer (still common)
npm install -D jest-environment-jsdom    # DOM env (was bundled pre-v28)
npm install -D @testing-library/jest-dom # DOM-aware matchers

Output: add-ons enabled by wiring into jest.config.{js,ts,cjs} (transform, testEnvironment, setupFilesAfterEach).

Versioning & Node support

  • Current major line is 29.x — released late 2022, still actively patched with monthly maintenance releases in 2025-2026.
  • A 30.x line is in beta as of 2026 — drops Node 14/16 support entirely, ships new circus runner defaults, and removes the legacy jest-jasmine2 runner.
  • Recent 29.x releases require Node 14.15+, 16.10+, or 18+. Node 20 is the recommended LTS target.
  • jest-environment-jsdom is unbundled since v28 — you must install it explicitly for any DOM-based tests.
  • Always a dev dependency — no runtime artefact ships.

Package metadata

  • Maintainer: Meta Open Source (originally; now community-led under the Jest org).
  • Project home: github.com/jestjs/jest
  • Docs: jestjs.io
  • npm: npmjs.com/package/jest
  • License: MIT
  • First released: 2014 (open-sourced); rewritten in 2016 (Jest 14+, the "modern" line).
  • Downloads: still tens of millions weekly — declining slowly as vitest and node:test eat share in greenfield work, but enormous in legacy CRA, Next.js (pre-15), and large monorepos.

Peer dependencies & extras

jest itself has no required peer dep — the binary works standalone. The friction is the transformer choice: by default Jest uses babel-jest with a permissive config that handles JS but stumbles on TS without help.

Add-onPurpose
ts-jestTypeScript transformer with type-checking. Slow but precise. Default for new TS projects until SWC took over.
@swc/jestSWC-Rust transformer. 10-20× faster than ts-jest, but skips type-checking (run tsc --noEmit separately).
babel-jestBabel transformer (auto-installed). Use for JSX / Babel plugin coverage.
jest-environment-jsdomSimulated DOM env. Required for any React/Vue component testing since v28.
jest-environment-nodeNode env (default). Use explicitly for SSR / API tests.
@testing-library/react (+ …/dom, …/jest-dom)Component-test queries and DOM matchers.
jest-mock-extendedStrongly-typed deep mocks. Better than raw jest.fn() for nested object shapes.
jest-junitJUnit XML reporter — required by most CI dashboards (GitLab, CircleCI, Jenkins).
mswNetwork mocking — same recommendation as Vitest; replaces jest.mock("axios") boilerplate.

Alternatives

RunnerTrade-off
vitestVite-native, Jest-compatible API, faster cold start, ESM-first. The clear winner for new Vite/TS projects. Migration is near drop-in for most suites.
node:test (stdlib)Built into Node 20+. Zero deps, zero config. Lacks snapshot system, UI, and watch ergonomics. Great for libraries.
mocha + chai + sinonThe pre-Jest classic. More à-la-carte; legacy mostly.
avaConcurrent-by-default, isolated processes. Tiny ecosystem next to Jest/Vitest.
bun testBuilt into Bun. Fastest startup. Lock-in to Bun runtime.
playwright/testE2E — complementary, not a replacement.

Honest take (2026): vitest is eating Jest's market share in greenfield work. Jest remains dominant in three places: (1) legacy Create-React-App / Next.js (pre-15) projects, (2) large monorepos where migration risk outweighs speed gains, and (3) React Native, where the Jest preset is the official path. New project starting today on Vite or modern TS? Pick vitest. Established Jest suite that works? No urgent reason to migrate.

Common gotchas

  1. jsdom is no longer bundled. Since v28, testEnvironment: "jsdom" requires npm install -D jest-environment-jsdom. Cryptic error: Cannot find module 'jest-environment-jsdom'.
  2. ESM support is opt-in and fragile. Jest defaults to CommonJS; ESM via --experimental-vm-modules works but breaks with circular deps, dynamic imports, and many CJS interop edge cases. This is the single biggest reason teams migrate to vitest.
  3. jest.mock is hoisted, factory closures break. jest.mock("./mod", () => ({ value: localVar })) errors because localVar is evaluated before any imports. Use jest.mock("./mod", () => { return { __esModule: true, default: jest.requireActual(...) }; }) patterns instead.
  4. Watch mode picks tests via git status. If you run Jest in a non-git directory or in CI without git history, --watch (vs --watchAll) silently runs zero tests. Use --watchAll to force.
  5. Coverage instrumentation breaks source maps in some bundlers. --coverage injects Istanbul probes into transformed code; tools that re-bundle the output (e.g. Vite preview) lose accurate stack traces.
  6. testMatch and roots interact non-obviously. testMatch is relative to project root; roots restricts the directories Jest descends into. Combining them with monorepo projects is the most common config foot-gun.
  7. Snapshot files are committed; CI updates are NOT auto. Snapshots in __snapshots__/ belong in git. jest -u updates locally; on CI you'll get failures unless you explicitly opt-in to update.
  8. Per-test isolation is implicit. Each test file runs in a new V8 context; module-level singletons reset between files. Tests that pass individually but fail in suite order are almost always relying on global mutation.

Real-world recipes

Simple unit test

javascript
// src/math.js
export function add(a, b) { return a + b; }
javascript
// src/math.test.js
import { add } from "./math.js";

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

  test("handles zero", () => {
    expect(add(0, 0)).toBe(0);
  });
});
bash
npx jest

Output:

text
 PASS  src/math.test.js
  add
    ✓ adds positive numbers (3 ms)
    ✓ handles zero

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total

Mocking a module with jest.mock

A module mock replaces every export with a jest.fn() you can configure per-test. Use it to isolate the unit under test from network, filesystem, or expensive dependencies.

javascript
// src/user-service.js
import { fetchUser } from "./api.js";
export async function getUserName(id) {
  const user = await fetchUser(id);
  return user.name.toUpperCase();
}
javascript
// src/user-service.test.js
import { jest } from "@jest/globals";
import { getUserName } from "./user-service.js";
import { fetchUser } from "./api.js";

jest.mock("./api.js");

test("uppercases name", async () => {
  fetchUser.mockResolvedValue({ name: "alice" });
  expect(await getUserName(1)).toBe("ALICE");
  expect(fetchUser).toHaveBeenCalledWith(1);
});

Snapshot testing

A snapshot serialises a value to disk on the first run; subsequent runs diff against the stored snapshot. Best for stable, readable structures — bad for noisy DOM trees or anything with timestamps.

javascript
import { renderToString } from "react-dom/server";
import { Banner } from "./Banner.jsx";

test("renders banner", () => {
  expect(renderToString(<Banner title="Hello" />)).toMatchSnapshot();
});

First run creates __snapshots__/Banner.test.js.snap. To update after intentional changes:

bash
npx jest -u

Output: (none — updates snapshot files in place, exits 0)

For tiny outputs, prefer inline snapshots so the expected value lives next to the assertion:

javascript
expect(formatPrice(99.5)).toMatchInlineSnapshot(`"$99.50"`);

Coverage gate

A coverage threshold in jest.config.js makes the runner exit non-zero when any metric falls below the floor. Wire it as a required PR check.

javascript
// jest.config.js
export default {
  collectCoverage: true,
  coverageReporters: ["text", "lcov", "json-summary"],
  coverageThreshold: {
    global: {
      branches: 75,
      functions: 80,
      lines: 80,
      statements: 80,
    },
  },
};

setupFilesAfterEach for global mock cleanup

setupFilesAfterEach (added in v27) runs after the test framework is set up but before tests run — the canonical hook for beforeEach/afterEach global setup.

javascript
// jest.setup.js
import { jest, afterEach } from "@jest/globals";
import "@testing-library/jest-dom";

afterEach(() => {
  jest.clearAllMocks();
  jest.useRealTimers();
});
javascript
// jest.config.js
export default {
  setupFilesAfterEach: ["<rootDir>/jest.setup.js"],
};

React component test with React Testing Library

javascript
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { Counter } from "./Counter.jsx";

test("increments counter on click", async () => {
  render(<Counter />);
  await userEvent.click(screen.getByRole("button", { name: /increment/i }));
  expect(screen.getByText(/count: 1/i)).toBeInTheDocument();
});

Required config: testEnvironment: "jsdom" plus setupFilesAfterEach: ["@testing-library/jest-dom"].

Production deployment

Jest itself ships no runtime artefacts — it's a CI workhorse.

CI configuration

yaml
# .github/workflows/test.yml
- uses: actions/setup-node@v4
  with: { node-version: "20", cache: "npm" }
- run: npm ci
- run: npx jest --ci --reporters=default --reporters=jest-junit
  env:
    JEST_JUNIT_OUTPUT_FILE: ./junit.xml
- uses: actions/upload-artifact@v4
  with: { name: junit, path: junit.xml }

Output:

text
PASS src/utils.test.ts
PASS src/api.test.ts
Test Suites: 2 passed, 2 total
Tests:       11 passed, 11 total

Shard across runners

--shard=N/M deterministically partitions test files. Combine with a CI matrix for 4× parallelism:

yaml
strategy:
  matrix:
    shard: [1, 2, 3, 4]
steps:
  - run: npx jest --shard=${{ matrix.shard }}/4 --ci

Performance tuning

Jest is the slowest popular runner — its startup cost (transformer warm-up, worker forking) dominates small suites. Optimisations stack:

Switch to SWC

ts-jest invokes the TypeScript compiler for every file. @swc/jest uses SWC and skips type-checking (you run tsc --noEmit separately):

javascript
// jest.config.js
export default {
  transform: { "^.+\\.(t|j)sx?$": "@swc/jest" },
};

Typical speedup on a 500-file TS suite: 4-10× cold, 2-3× warm.

--maxWorkers tuning

Default is os.cpus().length - 1. On CI runners with limited RAM, drop to --maxWorkers=2 to avoid OOM. On 8-core dev machines, --maxWorkers=50% is often faster than the default because it leaves room for transformer caching.

testPathIgnorePatterns aggressively

Excluding node_modules, dist, coverage, and build artefacts shaves directory-walk time on monorepos:

javascript
testPathIgnorePatterns: ["/node_modules/", "/dist/", "/coverage/", "/build/"]

Cache directory

Jest caches transformed files in $TMPDIR/jest_*. On CI, mount this to a cached layer:

yaml
- uses: actions/cache@v4
  with:
    path: /tmp/jest_cache
    key: jest-${{ hashFiles('**/package-lock.json') }}

Version migration guide

From → ToHighlights
26 → 27jest-circus becomes default test runner. Faster, better error messages. jest-jasmine2 deprecated.
27 → 28Drops Node 10 / 12. jest-environment-jsdom no longer bundled — install explicitly. Breaking change for every React/Vue project.
28 → 29Drops Node 14.14-. Snapshots now use newer pretty-format (whitespace-sensitive — expect to update most snapshots once). --snapshotFormat flag back-compat.
29 → 30 (beta)Drops Node 14/16. Removes jest-jasmine2 entirely. New default error reporter.

Migrating to vitest

A near-drop-in path when you're ready:

bash
npm uninstall jest ts-jest @types/jest babel-jest jest-environment-jsdom
npm install -D vitest @vitest/coverage-v8 jsdom

Output: (none — exits 0 on success)

jest.fnvi.fn, jest.mockvi.mock, everything else is identical. Use npx jest-to-vitest for codemod help.

Security considerations

  1. jest.setMock evaluates arbitrary code at config time. Treat jest.config.js like production code — never load from node_modules blindly.
  2. Snapshot serializers run on every assertion. A malicious prettyFormat serializer in a transitive dep can exfiltrate test data. Pin major versions, review serializer additions in PRs.
  3. --coverage reports may include source code. LCOV files include source content. Don't upload coverage artefacts from a private repo to public Codecov/Coveralls without checking the visibility setting.
  4. testPathIgnorePatterns with node_modules is the default for a reason. Removing it can cause Jest to execute test files inside dependencies that may include credentialed fixtures.

Configuration patterns

Multi-project (monorepo)

javascript
// jest.config.js
export default {
  projects: [
    "<rootDir>/packages/api",
    "<rootDir>/packages/web",
    "<rootDir>/packages/shared",
  ],
};

Each sub-project has its own jest.config.js; the root config aggregates results.

Per-environment via testEnvironment docblock

javascript
/**
 * @jest-environment jsdom
 */
import { render } from "@testing-library/react";
// ...

Override per file without changing global config.

Troubleshooting common errors

  • Cannot find module 'jest-environment-jsdom' — install it: npm install -D jest-environment-jsdom (since v28).
  • SyntaxError: Cannot use import statement outside a module — ESM with no transformer. Either switch to CJS source, install @swc/jest / babel-jest, or use the experimental ESM flag with NODE_OPTIONS.
  • ReferenceError: Cannot access 'mock' before initialization — local variable referenced in jest.mock factory. Move the factory to a separate file or use jest.doMock after the imports.
  • Tests pass individually, fail together — global state leaking between files. Add jest.resetModules() to setupFilesAfterEach, or set resetModules: true in config.
  • Jest did not exit one second after the test run — open handle (DB, server, timer). Run with --detectOpenHandles to find it.

Ecosystem integrations

ToolWhat it adds
@testing-library/react / vue / svelteComponent query utilities; framework-agnostic
@testing-library/jest-domDOM-aware matchers (toBeInTheDocument, toHaveAttribute)
jest-junitJUnit XML reporter for CI dashboards
jest-mock-extendedStrongly-typed deep mocks
mswNetwork mocking — replaces jest.mock("axios")
jest-watch-typeaheadBetter watch-mode filename / testname picker
@quramy/jest-prismaPrisma transaction-rollback per test
jest-image-snapshotVisual regression — pixel-diff snapshots

When NOT to use this

  • New Vite-based or modern TS projects. Reach for vitest — same API, faster, ESM-native.
  • Pure Node libraries. Node 20+'s node:test is enough and ships with zero deps.
  • E2E flows. Use Playwright or Cypress — Jest can't drive a real browser well.
  • ESM-only projects with native imports. Jest's ESM support is workable but fragile — vitest handles it natively.
  • Bun-first projects. bun test is faster and built in; only fall back to Jest if you need the plugin ecosystem.

Jest's strength is the plugin and matcher ecosystem — fifteen years of accumulated jest-extended, jest-axe, jest-image-snapshot, framework presets, etc. If your suite depends on niche plugins, the migration cost can outweigh the speed gain.

See also