Browse the Knowledge Hub211 resources
Jest review: tutorial, what it is, pros and cons
Jest is a JavaScript testing framework that bundles a test runner, assertions, mocking, snapshot testing and coverage, so a project can start testing with one dependency. Each test file runs in its own isolated module registry in a worker process. It is an OpenJS Foundation project under the MIT licence; the current major, Jest 30, has had regular minor releases, most recently 30.5.2 in September 2026.
QAble has evaluated this tool and works alongside it. The factual sections are complete; our view is bounded to what we have actually seen rather than a long-term verdict.
- last reviewed
- 13 October 2026
- version covered
- Jest 30
- licence
- Open source
At a glance
Jest at a glance
- Vendor
- OpenJS Foundation
- Category
- Unit testing
- Licence
- Open source (MIT)
- Platforms
- Node.js 18 and later, Browser APIs via jsdom
- Languages
- JavaScript, TypeScript
- Integrates with
- React Testing Library, Babel, GitHub Actions
- First released
- 2014
- Version reviewed
- Jest 30
Verdict
Our verdict on Jest
Strengths
- One install gives runner, assertions, mocks, fake timers, snapshots and coverage with no plugins to assemble
- Each test file gets its own module registry, so module-level state cannot leak between files
- jest.mock replaces whole modules, including automatic mocks of every export, without dependency injection
- Runs files in parallel worker processes and splits suites across CI machines with --shard
- Watch mode reruns only tests related to changed files, using the dependency graph
- Free under MIT and governed by the OpenJS Foundation
Weaknesses
- Native ES module support is still experimental and needs node --experimental-vm-modules
- Not supported inside Vite’s plugin pipeline, so Vite projects duplicate transform configuration
- Jest 30 removed matcher aliases such as toBeCalledWith that years of tutorials still use
- jsdom simulates the browser; layout, real rendering and browser quirks are not tested
Our take
Jest remains the most familiar JavaScript test framework, and for a CommonJS or Babel-based codebase, a React Native app or an existing large suite it is still a sound choice: the mocking is complete, the documentation is thorough and nearly every library documents a Jest setup. QAble has evaluated it rather than maintained a long-running suite on it, so treat this as a bounded take. The weak spot is the module system. Native ES modules are still experimental and need a Node flag, and Jest does not run inside Vite’s pipeline, so a Vite or ESM-first project usually fits Vitest better. Before choosing Jest for new work, check how your build tool, TypeScript setup and module format will be handled, because that decides the experience more than the test API does.
We have evaluated this tool rather than maintained a long-running suite on it, so this is a bounded view rather than a long-term verdict. The rest of this page is factual.
How it works
What is Jest and how does it work?
Workers, files and the module registry
Jest finds test files by pattern, then hands them to a pool of worker processes, by default one fewer than the number of cores in a single run. Inside a worker, each test file runs in its own environment with its own module registry: modules are loaded through Jest’s runtime rather than Node’s require cache, so a module imported in one file is a fresh instance in the next. That isolation is what makes jest.mock safe to use per file and why state leaks between files are rare. The cost is time, since every file re-evaluates its imports, which large dependency trees make noticeable.
Transforms before execution
Jest does not run your source directly. Each file passes through a transformer first, babel-jest by default whenever a Babel configuration exists, and the result is cached. TypeScript runs through Babel’s preset, which strips types without checking them, through ts-jest, which compiles with the TypeScript compiler, or, since 30.5.2, through Node’s own type stripping when no transformer claims a .ts file. Babel is also what hoists jest.mock calls above imports, which is why mocking behaves differently when Babel is not involved.
Environments: node or jsdom
testEnvironment decides which globals a test file sees. The default node environment suits server code. For code that touches the DOM, jest-environment-jsdom provides window, document and events through jsdom, a JavaScript implementation of web standards; it has not shipped with Jest since version 28 and is installed separately. jsdom does not lay out pages or render, so sizes, visibility computed from styles and real browser behaviour are out of reach. Jest 30 moved to jsdom 26, which changed some behaviour that older tests relied on.
Mocks, timers and snapshots built in
jest.fn creates a mock function that records calls and can return values or implementations. jest.mock replaces a module for the whole test file, with a factory or with an automatic mock of every export, and jest.spyOn wraps one method while keeping the original available. Fake timers replace setTimeout, Date and related APIs so time can be advanced deterministically. Snapshots serialise a value to a file on the first run and compare against it afterwards; in CI mode a missing snapshot fails instead of being written.
Strengths
What it does well
Module mocking without dependency injection
Because Jest controls module loading, jest.mock can replace a dependency that the code under test imports directly, such as an HTTP client or a payment SDK, without changing that code to accept it as a parameter. For legacy codebases that were not designed for testing, this is often what makes unit tests possible at all.
Per-argument mock behaviour
Jest 30.5 added mockFn.whenCalledWith, which sets a return value or implementation for specific arguments, including asymmetric matchers such as expect.any(Number). It replaces the branching mockImplementation functions that made tests hard to read when one mock had to answer differently per input.
Snapshot testing
toMatchSnapshot and toMatchInlineSnapshot record serialised output, such as a rendered component tree or an API payload, and flag any change. Inline snapshots are written into the test file itself, which keeps small expectations reviewable in the same diff as the code.
Selective and parallel runs
--findRelatedTests runs only tests that depend on given files, which suits pre-commit hooks, and watch mode does the same automatically. --shard splits test files across CI machines, and --maxWorkers controls the per-machine pool, as a number or a percentage of cores.
Projects for mixed codebases
The projects option runs several configurations in one invocation, such as server tests in the node environment and component tests in jsdom, with a combined report. Monorepos use it to run every package’s suite from the root.
Coverage with one flag
--coverage collects coverage through Babel instrumentation by default, or through V8’s built-in coverage with coverageProvider set to v8. Thresholds in the configuration fail the run when coverage drops below an agreed level.
Pricing
Jest pricing and licensing
Jest is free and open source under the MIT licence. It moved from Meta to the OpenJS Foundation in May 2022 and is maintained by community contributors, funded in part through Open Collective. There is no paid edition, no hosted service and no feature held back. The costs are CI time, which sharding and worker tuning reduce, and the engineering time needed for major upgrades and for keeping the transform setup in step with your build tooling.
- Licence: MIT, stated in the jestjs/jest repository.
- No tiers: mocking, snapshots, coverage, sharding and watch mode are all in the open-source packages on npm.
- Commercial support: none is offered by the project. Help comes from the documentation, the GitHub repository and the wider JavaScript community.
- Infrastructure: CI minutes scale with suite duration and memory; Jest 30 reduced memory use noticeably on large suites, according to its release post.
- Hidden cost: transforms. Babel, ts-jest or Node type stripping each need configuring and maintaining, and an ESM codebase may need extra work until Jest’s ESM support leaves experimental status.
Checked against the vendor’s own pages on 13 October 2026. Pricing changes; confirm on the official site before buying.
Getting started
Jest tutorial: getting started
Setup, step by step
- Use Node.js 18 or later; Jest 30 supports 18, 20, 22 and 24 and newer.
- Install Jest as a dev dependency, and jest-environment-jsdom too if tests touch the DOM.
- Run npm init jest@latest to generate a commented configuration file.
- Add "test": "jest" to the scripts in package.json.
- Create a file ending in .test.js next to the code it tests, or in __tests__.
- Run npm test, then npx jest --watch while you work.
Install
npm install --save-dev jest
# only if tests need window and document
npm install --save-dev jest-environment-jsdom
npm init jest@latestA first example
// src/cart.test.js
const { Cart } = require('./cart');
describe('Cart', () => {
test('sums line items in pence', () => {
const cart = new Cart();
cart.add('SKU-1', { quantity: 2, unitPence: 1500 });
cart.add('SKU-2', { quantity: 1, unitPence: 499 });
expect(cart.totalPence()).toBe(3499);
});
test('rejects a zero quantity', () => {
const cart = new Cart();
// Wrap the call so Jest can catch the throw
expect(() => cart.add('SKU-1', { quantity: 0, unitPence: 1500 }))
.toThrow('quantity must be positive');
});
test('loads saved carts', async () => {
// Return or await promises, or the test ends before they settle
await expect(Cart.load('missing-id')).rejects.toThrow('not found');
});
});What trips people up first
SyntaxError: Cannot use import statement outside a module. The project uses ES module syntax or TypeScript, and Jest is executing it as CommonJS because no transform handles it. For most projects the fix is a transformer: Babel with @babel/preset-env and @babel/preset-typescript, or ts-jest if you want type checking. Native ESM without a transform works only with node --experimental-vm-modules, transform set to {}, and jest.unstable_mockModule instead of jest.mock. A related trap: a dependency in node_modules that ships only ESM is not transformed by default, so add it to the transformIgnorePatterns exceptions.
In practice
Using Jest in practice
Configuration that scales past the first test
Keep configuration in a jest.config file rather than package.json once it grows, and wrap it in defineConfig, added in 30.3, for type checking and editor completion. Use projects to separate environments so server tests do not pay jsdom’s start-up cost. Keep global options such as reporters, coverage reporters and maxWorkers at the root: since 30.5.1 Jest warns when they appear in a project config, where they were always ignored. Turn on clearMocks so call history never carries from one test to the next, and add coverage thresholds once the baseline is known.
// jest.config.js
const { defineConfig } = require('jest');
module.exports = defineConfig({
clearMocks: true, // reset call history before every test
coverageProvider: 'v8',
coverageThreshold: { global: { lines: 80 } },
projects: [
{
displayName: 'server',
testEnvironment: 'node',
testMatch: ['<rootDir>/server/**/*.test.js'],
},
{
displayName: 'ui',
testEnvironment: 'jsdom', // needs jest-environment-jsdom installed
testMatch: ['<rootDir>/web/**/*.test.js'],
},
],
});Mocking dependencies without over-mocking
Mock at the boundary: network clients, payment SDKs, clocks and file systems, not the module’s own helpers. jest.mock with no factory replaces every export with a mock function, which is quick but returns undefined everywhere until you set behaviour; a factory, or jest.requireActual for the parts you keep, is more explicit. Assert on what the boundary received, with toHaveBeenCalledWith, rather than on how many internal calls happened. Use the toHave forms of matchers: the toBe and lastCalled aliases were removed in Jest 30.
// src/orders.test.js
const { placeOrder } = require('./orders');
const payments = require('./paymentClient');
// Automock: every export of paymentClient becomes a jest.fn()
jest.mock('./paymentClient');
test('charges the card once and marks the order paid', async () => {
payments.charge.mockResolvedValue({ id: 'ch_1', status: 'succeeded' });
const order = await placeOrder({ totalPence: 4200, card: 'tok_visa' });
// toHaveBeenCalledWith, not the removed toBeCalledWith alias
expect(payments.charge).toHaveBeenCalledTimes(1);
expect(payments.charge).toHaveBeenCalledWith(4200, 'tok_visa');
expect(order.status).toBe('paid');
});
test('reports a declined card', async () => {
payments.charge.mockRejectedValue(new Error('card_declined'));
await expect(placeOrder({ totalPence: 4200, card: 'tok_bad' }))
.rejects.toThrow('card_declined');
});Running Jest in GitHub Actions with sharding
Run with --ci so a missing snapshot fails instead of being written silently. Jest’s built-in github-actions reporter annotates failures on the pull request; configure it with the summary reporter. For a long suite, a matrix of jobs each passing --shard splits test files between machines, and --maxWorkers keeps each runner within its memory. Coverage from shards has to be merged afterwards if you need one report, so many teams collect coverage on a single unsharded job instead.
# .github/workflows/test.yml
name: test
on: [push, pull_request]
jobs:
jest:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3]
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24
cache: npm
- run: npm ci
# --ci: new snapshots fail instead of being written
- run: >
npx jest --ci --maxWorkers=50%
--shard=${{ matrix.shard }}/${{ strategy.job-total }}
--reporters=github-actions --reporters=summarySnapshots that stay useful
Snapshots work when they are small and reviewed. Snapshot a specific value, such as a serialised API payload or a short rendered fragment, rather than a whole page tree, and prefer inline snapshots for anything under a dozen lines so the expectation sits in the test. Review snapshot changes in pull requests as carefully as code, because running with -u accepts whatever the code now produces, correct or not. Jest 30 changed snapshot output in several places, such as printing an error’s cause, so expect a one-off update when upgrading.
Upgrading to Jest 30 and keeping it maintainable
Jest 30 needs Node 18 or later and TypeScript 5.4 or later for its types. Replace removed matcher aliases, which eslint-plugin-jest’s no-alias-methods rule can fix automatically, change --testPathPattern to --testPathPatterns, and replace jest.genMockFromModule with jest.createMockFromModule. Deep imports into Jest’s internal packages break, because each package is now bundled into one file. The jsdom upgrade can change DOM behaviour, so run the UI tests separately first. After that, keep upgrades routine: the 30.x minors have added defineConfig, --collectTests and better ESM handling on Node 24.9 and later without breaking changes.
Pitfalls
Common pitfalls
Tests that finish before their promises
Why it happens: An async assertion that is neither returned nor awaited lets the test function end first. The test passes, and the failure, if any, appears later or not at all.
The fix: Make the test async and await every promise, including expect(...).resolves and .rejects. The ESLint rules in eslint-plugin-jest flag unawaited assertions.
Matchers that are not a function after upgrading
Why it happens: Jest 30 removed aliases deprecated since Jest 26, including toBeCalled, toBeCalledWith, lastCalledWith, toReturn and toThrowError. Old tests and tutorials still use them.
The fix: Use toHaveBeenCalled, toHaveBeenCalledWith, toHaveBeenLastCalledWith, toHaveReturned and toThrow, or run eslint-plugin-jest’s no-alias-methods rule with autofix.
Assuming jsdom behaves like a browser
Why it happens: jsdom implements DOM APIs but not layout or rendering, so element sizes are zero and visibility from CSS is not computed. Tests pass while the real page is broken.
The fix: Test logic and markup in Jest, and cover layout, scrolling and real interaction in a browser, with Playwright or a browser-based component runner.
Mock state leaking between tests
Why it happens: Call history and implementations set in one test persist into the next unless something clears them, so assertions on call counts depend on test order.
The fix: Set clearMocks or restoreMocks in the configuration, or call jest.restoreAllMocks in afterEach, so each test starts from a known state.
ESM-only dependencies failing to load
Why it happens: transformIgnorePatterns excludes node_modules from transformation by default, so a package that ships only ES modules reaches Jest’s CommonJS runtime untransformed.
The fix: Add an exception for that package to transformIgnorePatterns, or use Jest’s experimental native ESM mode where the codebase is ESM throughout.
Committing snapshots no one reads
Why it happens: Large snapshots change on every markup tweak, reviewers stop reading them, and -u turns any output into the new expectation.
The fix: Keep snapshots small and specific, prefer inline snapshots, and assert directly on the values that matter where possible.
Compared
How Jest compares
| Tool | Included | Code transformation | Native ES modules | Real browser runs | Licence |
|---|---|---|---|---|---|
| Jest | Runner, assertions, mocks, snapshots, coverage | Babel by default, or ts-jest | Experimental, behind a Node flag | No; jsdom simulation | MIT |
| Vitest | Runner, assertions, mocks, snapshots, coverage | Vite pipeline and plugins | Yes | Browser Mode via Playwright or WebdriverIO | MIT |
| Mocha | Runner only; add Chai, Sinon | None built in | Yes | Runs in browsers as well as Node | MIT |
Head-to-head comparisons
Vitest vs JestFAQ
Frequently asked questions
What is Jest?
Jest is a JavaScript testing framework that bundles a test runner, assertions, mocking, snapshot testing and coverage, so a project can start testing with one dependency. Each test file runs in its own isolated module registry in a worker process. It is an OpenJS Foundation project under the MIT licence; the current major, Jest 30, has had regular minor releases, most recently 30.5.2 in September 2026.
Is Jest free?
Yes. Jest is open source under the MIT licence and has no paid edition, hosted service or feature tiers. It has been an OpenJS Foundation project since May 2022, after moving from Meta, and is maintained by community contributors. Your only costs are CI time and the engineering time to configure transforms and handle upgrades.
Jest vs Vitest: which should I choose?
For a project built with Vite, or one written as native ES modules, Vitest: it reuses the Vite configuration, handles ESM and TypeScript without extra transforms and has a largely Jest-compatible API. Jest remains a good choice for CommonJS or Babel-based codebases, React Native, and large existing suites where migration would cost more than it saves. The deciding factor is usually build tooling, not the test API.
Why does toBeCalledWith no longer work?
Jest 30 removed matcher aliases that had been deprecated since Jest 26. toBeCalledWith is now toHaveBeenCalledWith, toBeCalled is toHaveBeenCalled, toReturn is toHaveReturned and toThrowError is toThrow, with similar changes for the last and nth forms. eslint-plugin-jest’s no-alias-methods rule can rewrite them automatically.
Does Jest support ES modules?
Partly. Native ESM support is still marked experimental: it needs Node’s --experimental-vm-modules flag, an empty or ESM-emitting transform, and jest.unstable_mockModule for mocking. Recent 30.x releases improved ESM handling on Node 24.9 and later, including automocking. Most projects still let Babel or ts-jest compile ESM syntax to CommonJS for Jest.
How do I use Jest with TypeScript?
Choose a transform. Babel with @babel/preset-typescript is fast but does not type-check. ts-jest compiles through the TypeScript compiler and reports type errors. Since 30.5.2, Jest can also use Node’s built-in type stripping when no transformer claims a .ts file, which does not support enums, namespaces or JSX. For types in test files, import from @jest/globals or install @types/jest.
How do I speed up a slow Jest suite?
Check where time goes first: large import trees re-evaluated in every file are a common cause. Use the node environment wherever the DOM is not needed, split jsdom tests into their own project, tune --maxWorkers to the machine, and shard across CI jobs with --shard. Locally, watch mode and --findRelatedTests run only the tests affected by your changes.
Is Jest still maintained?
Yes. Jest 30 was released in June 2025, and minor releases have continued since, with 30.3 in March 2026, 30.4 in May and 30.5 at the end of August, followed by patch releases in September. Recent minors added defineConfig, per-argument mocks with whenCalledWith and better ESM handling on current Node versions.
Alternatives
Jest alternatives worth weighing
Vitest
JavaScript and TypeScript test framework built on Vite, reusing its config and plugins, with a Jest-compatible API and a real-browser mode.
Mocha
Flexible JavaScript test framework that supplies structure and lets you choose assertions and mocking.
Resources
Where to go next
Picking the wrong tool costs more than picking late.
QAble advises on toolchain decisions and then implements them, with engineers who have run these tools on real delivery. Start with a free QA audit.