Jest is the most widely used JavaScript testing framework. It works with Vanilla JS, TypeScript, React, Node.js, and most modern JS toolchains with zero configuration. This cheat sheet covers every Jest feature you need to write reliable tests.
Quick reference
| Task | Jest API |
|---|---|
| Define a test suite | describe('Name', () => { ... }) |
| Define a test | it('does X', () => { ... }) / test(...) |
| Assert equality | expect(val).toBe(expected) |
| Deep equality | expect(obj).toEqual({ a: 1 }) |
| Negate matcher | expect(val).not.toBe(x) |
| Mock a function | jest.fn() |
| Mock a module | jest.mock('./module') |
| Spy on a method | jest.spyOn(obj, 'method') |
| Run before each test | beforeEach(() => { ... }) |
| Run after all tests | afterAll(() => { ... }) |
| Skip a test | it.skip(...) / xit(...) |
| Focus one test | it.only(...) / fit(...) |
| Async test | it('...', async () => { await ... }) |
| Assert throws | expect(() => fn()).toThrow() |
| Snapshot | expect(component).toMatchSnapshot() |
| Coverage | jest --coverage |
Installation and setup
# New project
npm install --save-dev jest
# TypeScript support
npm install --save-dev jest @types/jest ts-jest
# React / jsdom environment
npm install --save-dev jest jest-environment-jsdom @testing-library/react
jest.config.ts (TypeScript):
import type { Config } from 'jest';
const config: Config = {
preset: 'ts-jest',
testEnvironment: 'node', // or 'jsdom' for browser code
clearMocks: true, // reset mocks between tests
coverageThreshold: {
global: { lines: 80 },
},
};
export default config;
package.json scripts:
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"test:ci": "jest --ci --coverage --forceExit"
}
}
Test structure
describe('Calculator', () => {
// Runs once before all tests in this suite
beforeAll(() => {
// expensive setup (DB connection, server start)
});
// Runs before each test
beforeEach(() => {
// reset state, clear mocks
});
afterEach(() => {
// cleanup per test
});
afterAll(() => {
// teardown (close DB, stop server)
});
it('adds two numbers', () => {
expect(add(2, 3)).toBe(5);
});
it('throws on division by zero', () => {
expect(() => divide(1, 0)).toThrow('Division by zero');
});
});
Nested suites
describe('UserService', () => {
describe('create()', () => {
it('returns a new user', () => { ... });
it('throws if email exists', () => { ... });
});
describe('delete()', () => {
it('removes the user', () => { ... });
});
});
Matchers
Equality
expect(2 + 2).toBe(4); // strict ===
expect({ a: 1 }).toEqual({ a: 1 }); // deep equal (objects/arrays)
expect({ a: 1, b: 2 }).toMatchObject({ a: 1 }); // partial match
expect(null).toBeNull();
expect(undefined).toBeUndefined();
expect('hello').toBeDefined();
expect(true).toBeTruthy();
expect(0).toBeFalsy();
Numbers
expect(5).toBeGreaterThan(3);
expect(5).toBeGreaterThanOrEqual(5);
expect(3).toBeLessThan(5);
expect(3).toBeLessThanOrEqual(3);
expect(0.1 + 0.2).toBeCloseTo(0.3, 5); // floating point
Strings
expect('hello world').toContain('world');
expect('hello').toMatch(/^hel/);
expect('hello').toMatch('ell');
Arrays and iterables
expect([1, 2, 3]).toContain(2);
expect([1, 2, 3]).toHaveLength(3);
expect([{ id: 1 }, { id: 2 }]).toContainEqual({ id: 1 }); // deep
expect(new Set([1, 2])).toEqual(new Set([1, 2]));
Errors
expect(() => JSON.parse('bad')).toThrow();
expect(() => JSON.parse('bad')).toThrow(SyntaxError);
expect(() => fn()).toThrow('expected message');
expect(() => fn()).toThrow(/pattern/);
Negation
expect(null).not.toBeDefined();
expect([1, 2]).not.toContain(3);
expect(() => safeOp()).not.toThrow();
Mock functions (jest.fn)
A mock function records every call — arguments, return values, and this context.
const mockFn = jest.fn();
mockFn(1, 2);
mockFn('hello');
expect(mockFn).toHaveBeenCalled();
expect(mockFn).toHaveBeenCalledTimes(2);
expect(mockFn).toHaveBeenCalledWith(1, 2);
expect(mockFn).toHaveBeenLastCalledWith('hello');
expect(mockFn).toHaveBeenNthCalledWith(1, 1, 2); // first call
Controlling return values
const fn = jest.fn();
fn.mockReturnValue('default');
fn.mockReturnValueOnce('first').mockReturnValueOnce('second');
fn.mockResolvedValue({ data: [] }); // resolves a Promise
fn.mockRejectedValue(new Error('fail')); // rejects a Promise
fn.mockResolvedValueOnce({ data: [1] }); // one-time resolved value
fn.mockImplementation((x: number) => x * 2);
fn.mockImplementationOnce(() => 42);
Inspecting calls
console.log(mockFn.mock.calls); // [[1,2], ['hello']]
console.log(mockFn.mock.results); // [{ type:'return', value:... }, ...]
console.log(mockFn.mock.instances); // `this` for each call
Reset and restore
mockFn.mockClear(); // clears calls/results, keeps implementation
mockFn.mockReset(); // also removes implementation
mockFn.mockRestore(); // only works for spies (restores original)
Spying on existing methods
const user = {
getName() { return 'Alice'; },
};
const spy = jest.spyOn(user, 'getName');
spy.mockReturnValue('Bob');
expect(user.getName()).toBe('Bob');
expect(spy).toHaveBeenCalled();
spy.mockRestore(); // puts 'Alice' back
Spy on a module method:
import * as fs from 'fs';
jest.spyOn(fs, 'readFileSync').mockReturnValue('mocked content');
Mocking modules
Auto-mock entire module
jest.mock('./database');
// All exports are replaced with jest.fn()
import { getUser } from './database';
(getUser as jest.Mock).mockResolvedValue({ id: 1, name: 'Alice' });
Manual mock with factory
jest.mock('./emailService', () => ({
sendEmail: jest.fn().mockResolvedValue({ sent: true }),
validateAddress: jest.fn().mockReturnValue(true),
}));
Partial mock (keep some real)
jest.mock('./utils', () => {
const real = jest.requireActual('./utils');
return {
...real,
formatDate: jest.fn().mockReturnValue('2026-01-01'),
};
});
Mock Node modules
jest.mock('axios', () => ({
get: jest.fn().mockResolvedValue({ data: { id: 1 } }),
post: jest.fn().mockResolvedValue({ data: { created: true } }),
}));
__mocks__ directory (automatic)
Place __mocks__/fs.ts next to node_modules/ and Jest picks it up automatically when jest.mock('fs') is called.
Async tests
async/await (preferred)
it('fetches user data', async () => {
const user = await fetchUser(1);
expect(user.name).toBe('Alice');
});
Resolves / rejects matchers
it('resolves with data', async () => {
await expect(fetchUser(1)).resolves.toEqual({ id: 1, name: 'Alice' });
});
it('rejects on error', async () => {
await expect(fetchUser(-1)).rejects.toThrow('Not found');
});
Fake timers
beforeEach(() => { jest.useFakeTimers(); });
afterEach(() => { jest.useRealTimers(); });
it('calls callback after delay', () => {
const cb = jest.fn();
setTimeout(cb, 1000);
expect(cb).not.toHaveBeenCalled();
jest.advanceTimersByTime(1000);
expect(cb).toHaveBeenCalledTimes(1);
});
it('runs all pending timers', () => {
jest.runAllTimers();
});
Snapshots
Snapshots catch unexpected UI or data structure changes.
import { render } from '@testing-library/react';
import Button from './Button';
it('renders correctly', () => {
const { container } = render(<Button label="Click me" />);
expect(container).toMatchSnapshot();
});
Update snapshots when a change is intentional:
jest --updateSnapshot # or: jest -u
Inline snapshots
expect({ name: 'Alice', role: 'admin' }).toMatchInlineSnapshot(`
{
"name": "Alice",
"role": "admin",
}
`);
Testing React components
npm install --save-dev @testing-library/react @testing-library/user-event
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import LoginForm from './LoginForm';
describe('LoginForm', () => {
it('shows error on empty submit', async () => {
const user = userEvent.setup();
render(<LoginForm onLogin={jest.fn()} />);
await user.click(screen.getByRole('button', { name: /login/i }));
expect(screen.getByText(/email is required/i)).toBeInTheDocument();
});
it('calls onLogin with credentials', async () => {
const onLogin = jest.fn().mockResolvedValue(undefined);
const user = userEvent.setup();
render(<LoginForm onLogin={onLogin} />);
await user.type(screen.getByLabelText(/email/i), 'alice@example.com');
await user.type(screen.getByLabelText(/password/i), 'secret');
await user.click(screen.getByRole('button', { name: /login/i }));
expect(onLogin).toHaveBeenCalledWith({
email: 'alice@example.com',
password: 'secret',
});
});
});
Testing API handlers (Node.js)
import request from 'supertest';
import app from '../app';
import { db } from '../db';
jest.mock('../db');
describe('GET /users/:id', () => {
it('returns 200 with user', async () => {
(db.findUser as jest.Mock).mockResolvedValue({ id: '1', name: 'Alice' });
const res = await request(app).get('/users/1');
expect(res.status).toBe(200);
expect(res.body).toEqual({ id: '1', name: 'Alice' });
});
it('returns 404 when not found', async () => {
(db.findUser as jest.Mock).mockResolvedValue(null);
const res = await request(app).get('/users/999');
expect(res.status).toBe(404);
});
});
Coverage
jest --coverage
Configure thresholds in jest.config.ts:
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
'./src/auth/': {
lines: 95, // stricter threshold for critical paths
},
},
coveragePathIgnorePatterns: ['/node_modules/', '/dist/', '*.d.ts'],
Coverage reporters:
coverageReporters: ['text', 'lcov', 'html'],
Common patterns
Factory helpers
function makeUser(overrides: Partial<User> = {}): User {
return {
id: '1',
name: 'Alice',
email: 'alice@example.com',
role: 'user',
...overrides,
};
}
it('admin can delete posts', () => {
const admin = makeUser({ role: 'admin' });
expect(canDelete(admin)).toBe(true);
});
Testing error boundaries
it('returns error for invalid input', async () => {
const result = await processData(null);
expect(result.success).toBe(false);
expect(result.error).toMatch(/invalid/i);
});
Parameterized tests with test.each
test.each([
[1, 1, 2],
[0, 0, 0],
[-1, 1, 0],
])('add(%i, %i) = %i', (a, b, expected) => {
expect(add(a, b)).toBe(expected);
});
// With objects
test.each([
{ input: 'hello', expected: 'HELLO' },
{ input: '', expected: '' },
])('toUpperCase("$input") = "$expected"', ({ input, expected }) => {
expect(input.toUpperCase()).toBe(expected);
});
Common mistakes
| Mistake | Problem | Fix |
|---|---|---|
expect(promise) without await |
Test always passes — assertion never runs | await expect(promise).resolves.toBe(...) |
Forgetting return in promise test |
Same — unhandled rejection silently ignored | Use async/await instead |
toBe for objects/arrays |
{} !== {} in JS — reference check fails |
Use toEqual for deep equality |
| Mocking after import | Jest hoists jest.mock() — mocking after import has no effect |
Always put jest.mock() at top level |
| Not clearing mocks between tests | State leaks cause flaky tests | Set clearMocks: true in config |
jest.fn() inside describe without reset |
Mock accumulates calls across tests | Use beforeEach to recreate or mockClear() |
| Snapshot tests without thinking | Snapshots of large trees break on any change | Snapshot only stable, minimal output |
TypeScript tips
// Type the mock
const mockFetch = fetch as jest.MockedFunction<typeof fetch>;
mockFetch.mockResolvedValue(new Response(JSON.stringify({ id: 1 })));
// Mock a class
jest.mock('./UserRepository');
const MockRepo = UserRepository as jest.MockedClass<typeof UserRepository>;
MockRepo.prototype.findById.mockResolvedValue({ id: '1', name: 'Alice' });
// Assert mock calls with types
expect(mockFn).toHaveBeenCalledWith<[string, number]>('key', 42);
Frequently asked questions
What's the difference between describe, it, and test?it and test are identical — choose whichever reads better ("it should..." vs "test that..."). describe groups related tests into a suite.
When should I use toBe vs toEqual?
Use toBe for primitives (string, number, boolean, null, undefined). Use toEqual for objects and arrays — it compares values recursively, not references.
How do I run a single test file?jest path/to/file.test.ts or jest --testPathPattern="auth" to match by name pattern.
How do I debug a failing test?
Add --verbose to see each test name. Use console.log inside tests. Run node --inspect-brk node_modules/.bin/jest --runInBand and attach a debugger. VS Code has built-in Jest debugging via the "Jest Runner" extension.
What's --runInBand for?
It runs all tests serially in the main process (no worker threads). Useful for debugging and for tests that share global state like a real database.
How do I test code that uses Date.now() or Math.random()?
Mock them: jest.spyOn(Date, 'now').mockReturnValue(1700000000000) or jest.spyOn(Math, 'random').mockReturnValue(0.5). For Date, jest.useFakeTimers() also controls new Date().