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
# 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.
# 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.xline is in beta as of 2026 — drops Node 14/16 support entirely, ships new circus runner defaults, and removes the legacyjest-jasmine2runner. - Recent
29.xreleases require Node 14.15+, 16.10+, or 18+. Node 20 is the recommended LTS target. jest-environment-jsdomis 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:testeat 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-on | Purpose |
|---|---|
ts-jest | TypeScript transformer with type-checking. Slow but precise. Default for new TS projects until SWC took over. |
@swc/jest | SWC-Rust transformer. 10-20× faster than ts-jest, but skips type-checking (run tsc --noEmit separately). |
babel-jest | Babel transformer (auto-installed). Use for JSX / Babel plugin coverage. |
jest-environment-jsdom | Simulated DOM env. Required for any React/Vue component testing since v28. |
jest-environment-node | Node env (default). Use explicitly for SSR / API tests. |
@testing-library/react (+ …/dom, …/jest-dom) | Component-test queries and DOM matchers. |
jest-mock-extended | Strongly-typed deep mocks. Better than raw jest.fn() for nested object shapes. |
jest-junit | JUnit XML reporter — required by most CI dashboards (GitLab, CircleCI, Jenkins). |
msw | Network mocking — same recommendation as Vitest; replaces jest.mock("axios") boilerplate. |
Alternatives
| Runner | Trade-off |
|---|---|
| vitest | Vite-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 + sinon | The pre-Jest classic. More à-la-carte; legacy mostly. |
| ava | Concurrent-by-default, isolated processes. Tiny ecosystem next to Jest/Vitest. |
| bun test | Built into Bun. Fastest startup. Lock-in to Bun runtime. |
| playwright/test | E2E — 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
jsdomis no longer bundled. Since v28,testEnvironment: "jsdom"requiresnpm install -D jest-environment-jsdom. Cryptic error:Cannot find module 'jest-environment-jsdom'.- ESM support is opt-in and fragile. Jest defaults to CommonJS; ESM via
--experimental-vm-modulesworks but breaks with circular deps, dynamic imports, and many CJS interop edge cases. This is the single biggest reason teams migrate to vitest. jest.mockis hoisted, factory closures break.jest.mock("./mod", () => ({ value: localVar }))errors becauselocalVaris evaluated before any imports. Usejest.mock("./mod", () => { return { __esModule: true, default: jest.requireActual(...) }; })patterns instead.- 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--watchAllto force. - Coverage instrumentation breaks source maps in some bundlers.
--coverageinjects Istanbul probes into transformed code; tools that re-bundle the output (e.g. Vite preview) lose accurate stack traces. testMatchandrootsinteract non-obviously.testMatchis relative to project root;rootsrestricts the directories Jest descends into. Combining them with monorepoprojectsis the most common config foot-gun.- Snapshot files are committed; CI updates are NOT auto. Snapshots in
__snapshots__/belong in git.jest -uupdates locally; on CI you'll get failures unless you explicitly opt-in to update. - 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
// src/math.js
export function add(a, b) { return a + b; }
// 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);
});
});
npx jest
Output:
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.
// src/user-service.js
import { fetchUser } from "./api.js";
export async function getUserName(id) {
const user = await fetchUser(id);
return user.name.toUpperCase();
}
// 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.
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:
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:
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.
// 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.
// jest.setup.js
import { jest, afterEach } from "@jest/globals";
import "@testing-library/jest-dom";
afterEach(() => {
jest.clearAllMocks();
jest.useRealTimers();
});
// jest.config.js
export default {
setupFilesAfterEach: ["<rootDir>/jest.setup.js"],
};
React component test with React Testing Library
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
# .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:
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:
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):
// 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:
testPathIgnorePatterns: ["/node_modules/", "/dist/", "/coverage/", "/build/"]
Cache directory
Jest caches transformed files in $TMPDIR/jest_*. On CI, mount this to a cached layer:
- uses: actions/cache@v4
with:
path: /tmp/jest_cache
key: jest-${{ hashFiles('**/package-lock.json') }}
Version migration guide
| From → To | Highlights |
|---|---|
| 26 → 27 | jest-circus becomes default test runner. Faster, better error messages. jest-jasmine2 deprecated. |
| 27 → 28 | Drops Node 10 / 12. jest-environment-jsdom no longer bundled — install explicitly. Breaking change for every React/Vue project. |
| 28 → 29 | Drops 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:
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.fn → vi.fn, jest.mock → vi.mock, everything else is identical. Use npx jest-to-vitest for codemod help.
Security considerations
jest.setMockevaluates arbitrary code at config time. Treatjest.config.jslike production code — never load fromnode_modulesblindly.- Snapshot serializers run on every assertion. A malicious
prettyFormatserializer in a transitive dep can exfiltrate test data. Pin major versions, review serializer additions in PRs. --coveragereports 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.testPathIgnorePatternswithnode_modulesis 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)
// 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
/**
* @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 injest.mockfactory. Move the factory to a separate file or usejest.doMockafter the imports.- Tests pass individually, fail together — global state leaking between files. Add
jest.resetModules()tosetupFilesAfterEach, or setresetModules: truein config. Jest did not exit one second after the test run— open handle (DB, server, timer). Run with--detectOpenHandlesto find it.
Ecosystem integrations
| Tool | What it adds |
|---|---|
@testing-library/react / vue / svelte | Component query utilities; framework-agnostic |
@testing-library/jest-dom | DOM-aware matchers (toBeInTheDocument, toHaveAttribute) |
jest-junit | JUnit XML reporter for CI dashboards |
jest-mock-extended | Strongly-typed deep mocks |
msw | Network mocking — replaces jest.mock("axios") |
jest-watch-typeahead | Better watch-mode filename / testname picker |
@quramy/jest-prisma | Prisma transaction-rollback per test |
jest-image-snapshot | Visual 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:testis 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 testis 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
- JavaScript: jest — full CLI, config patterns, mocking guide
- Packages: npm-vitest — the modern alternative
- Packages: npm-playwright — E2E layer
- Packages: npm-cypress — alternative E2E
- Concept: API — testing the contract, not the implementation