Jest
The default JavaScript test runner — configuration, projects, snapshot testing, fake timers, and the choices that keep a large Jest suite fast.
1 min read · updated 19 September 2026
Jest is the default JavaScript and TypeScript test runner: assertions, mocking, coverage, snapshots and a watch mode in one package, with zero config for most projects.
#The configuration that matters
// jest.config.js
module.exports = {
// SWC, not ts-jest: type checking belongs in `tsc --noEmit`, not in the
// hot path of every test run. This alone often halves suite time.
transform: { '^.+\\.(t|j)sx?$': '@swc/jest' },
moduleNameMapper: { '^@/(.*)$': '<rootDir>/$1' },
// Node by default; jsdom only where the DOM is genuinely needed.
projects: [
{
displayName: 'node',
testEnvironment: 'node',
testMatch: ['<rootDir>/lib/**/*.test.ts']
},
{
displayName: 'dom',
testEnvironment: 'jsdom',
testMatch: ['<rootDir>/components/**/*.test.tsx'],
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts']
}
],
collectCoverageFrom: ['lib/**/*.ts', 'components/**/*.tsx', '!**/*.d.ts'],
coverageReporters: ['text-summary', 'lcov'],
// Fail on an unexpected console.error — usually a React warning that
// nobody would otherwise read.
setupFilesAfterEach: []
};Projects are the most underused feature. jsdom costs roughly 100ms of
setup per test file; running pure logic tests in the node environment
instead is free speed.
#Parameterised tests
// One case per row, each reported separately with its values in the name.
it.each([
[12_000, 2_400],
[10_000, 2_000], // the boundary — the case most often missing
[9_999, 0]
])('discounts an order of %i by %i', (subtotal, expected) => {
expect(applyDiscount({ subtotalCents: subtotal }, policy).discountCents).toBe(expected);
});
// The tagged-template form, when the columns want names
it.each`
tier | shippingCents
${'standard'} | ${395}
${'gold'} | ${0}
`('charges $shippingCents shipping for a $tier customer', ({ tier, shippingCents }) => {
expect(shippingFor(anOrder({ tier })).cents).toBe(shippingCents);
});#Fake timers
The cure for tests that wait, and for logic involving debounce, retry backoff or expiry.
describe('retry with backoff', () => {
beforeEach(() => jest.useFakeTimers());
afterEach(() => jest.useRealTimers());
it('waits a second, then two, then gives up', async () => {
const attempt = jest.fn()
.mockRejectedValueOnce(new Error('503'))
.mockRejectedValueOnce(new Error('503'))
.mockResolvedValue('ok');
const promise = retryWithBackoff(attempt, { retries: 2 });
await jest.advanceTimersByTimeAsync(1_000);
await jest.advanceTimersByTimeAsync(2_000);
await expect(promise).resolves.toBe('ok');
expect(attempt).toHaveBeenCalledTimes(3);
});
});advanceTimersByTimeAsync is the one to reach for when the code under test
awaits between timers; the synchronous version leaves promise callbacks
unflushed and produces confusing hangs.
Setting a fixed system time is the other half:
jest.useFakeTimers({ now: new Date('2026-01-01T12:00:00Z') });#Matchers worth knowing
expect(order).toMatchObject({ status: 'paid' }); // partial — ignores extra keys
expect(items).toEqual(expect.arrayContaining([expect.objectContaining({ sku: 'a' })]));
expect(fn).toHaveBeenCalledWith(expect.stringMatching(/^ORD-/));
await expect(client.fetch()).rejects.toThrow(RateLimitError);
expect(total).toBeCloseTo(12.34, 2); // floatstoMatchObject is the workhorse. Asserting on a whole object with toEqual
makes the test fail whenever an unrelated field is added, which is a slow
way to make a suite hostile to change.
#Snapshots, carefully
// Inline snapshots keep the expected value next to the assertion, which
// makes a diff in review readable. Prefer them to external .snap files.
expect(formatInvoice(invoice)).toMatchInlineSnapshot(`
"Invoice INV-1
Field Notes x2 £24.00
Shipping £3.95
Total £27.95"
`);The failure mode is well known — see snapshot testing — and the mitigation is to snapshot small, meaningful output rather than whole component trees.
#Keeping a large suite fast
- Replace ts-jest with @swc/jest. Frequently the single biggest win.
- Use the node environment where you can (via
projects). --maxWorkers=50%in CI. The default over-subscribes a two-core runner and makes everything slower.--onlyChangedon pull requests, full suite on main.- Find the slow files:
jest --listTestsplus the--verbosetimings, or--detectOpenHandleswhen a suite hangs at the end (almost always an unclosed server or timer).
#Where Jest ends
Jest is a unit and component runner. Browser journeys belong in Playwright; integration tests against a real database belong in a suite with Testcontainers and a longer timeout. Mocking is covered separately in jest mocking, because it is the part most often overused.
Common questions
- Should I use Jest or Vitest?
- For a Vite-based project, Vitest — it shares the build pipeline, starts faster and needs almost no configuration. For everything else, Jest remains the safer default: it has the larger ecosystem, more mature Node support and far more answers on the internet.
- Why are my Jest tests slow?
- Usually the transform. TypeScript compiled through ts-jest on every file is the single biggest cost; switching to SWC or Babel typically cuts a suite's runtime in half. After that, look at jsdom — tests that do not touch the DOM should run in the node environment.
- What is the difference between jest.mock and jest.spyOn?
- jest.mock replaces a whole module before it is imported, which is hoisted to the top of the file. jest.spyOn wraps an existing method on an object you already have, and can be restored. Prefer spyOn when you have a reference; it is easier to reason about and does not leak across tests.
Runnable samples for this page
last test results ↗- TypeScript
typescript/src/tools/jest
Working tests, not fragments — they run in CI on every push to 8exgh/endtoendtester-samples.
Was this page useful?
Related topics
- Jest MockingModule mocks, spies, manual mocks and timer control — how Jest's mocking works, and the patterns that keep it from taking over a suite.
- VitestA Vite-native test runner with a Jest-compatible API — faster startup, native ESM and TypeScript, and the cases where it is the better default.
- Snapshot TestingRecording output and comparing it on every run — where snapshots earn their place, the approval reflex that destroys their value, and better alternatives.
- Testing ReactComponents, hooks, context, server components and async state — what to test in a React application, what to leave alone, and how to avoid act warnings.
- Writing Tests in TypeScriptThe TypeScript testing toolchain — Jest or Vitest, typed doubles, async idioms, and the type-level tricks that make tests both safer and more readable.
- Code CoverageWhat the percentage measures, why it is a finding tool rather than a target, how to collect it in each ecosystem, and how to gate on it without causing harm.
- Arrange-Act-AssertThe three-part shape every readable test has, why the act step should be one line, and the smells that show up when a test will not fit the pattern.
- PlaywrightThe default browser automation tool in 2026 — auto-waiting locators, tracing, sharding and fixtures — with the configuration that matters and the mistakes that still cause flakiness.