TL;DR
Jest is a zero-configuration JavaScript testing framework with built-in mocking, snapshot testing, code coverage via Istanbul, and jsdom for DOM simulation. Pair it with React Testing Library for component tests, use jest.fn() and jest.mock() for mocking, and run --coverage to enforce thresholds in CI.
Key Takeaways
- Jest works out of the box with zero configuration for most JavaScript and TypeScript projects
- Use
describe/itblocks andexpectmatchers for expressive, readable tests jest.fn(),jest.mock(), andjest.spyOn()cover virtually all mocking scenarios- React Testing Library encourages testing behavior over implementation details
- Enforce coverage thresholds in CI with
--coverageandcoverageThreshold - Fake timers let you test debounce, throttle, and polling without real delays
- Vitest is a fast drop-in alternative for Vite-based projects, sharing most of Jest's API
What Is Jest? Zero-Config Testing for JavaScript
Jest is a JavaScript testing framework maintained by Meta (formerly Facebook). It was designed with developer experience as the top priority: install it, run npx jest, and it finds and runs every *.test.js, *.spec.js, or file inside a __tests__ folder automatically — no config file required for most projects.
Under the hood, Jest ships with several integrated capabilities that other frameworks require you to assemble yourself:
- Test runner — finds, executes, and reports results for all test files in parallel using worker processes
- Assertion library — the
expectAPI with 30+ built-in matchers plus third-party extensions viaexpect.extend() - Mocking system —
jest.fn(),jest.mock(), auto-mocking, and manual mocks via__mocks__directories - Snapshot testing — serialize any JavaScript value to a text file and detect unintended changes across commits
- Code coverage — powered by Istanbul/NYC, reports lines, statements, branches, and functions with threshold enforcement
- jsdom environment — simulates a browser DOM in Node.js so you can test React, Vue, and Angular components without a real browser
- Fake timers — replace
setTimeout,setInterval, andDatewith controllable test versions
Installation
# npm
npm install --save-dev jest
# TypeScript support
npm install --save-dev jest @types/jest ts-jest
# For React + TypeScript projects
npm install --save-dev jest @types/jest ts-jest jest-environment-jsdom @testing-library/react @testing-library/jest-dom @testing-library/user-eventjest.config.ts (TypeScript)
import type { Config } from 'jest';
const config: Config = {
preset: 'ts-jest',
testEnvironment: 'jsdom', // use 'node' for server-side tests
setupFilesAfterFramework: ['<rootDir>/jest.setup.ts'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1', // path alias support
'\.css$': '<rootDir>/__mocks__/styleMock.js',
'\.svg$': '<rootDir>/__mocks__/svgMock.js',
},
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
collectCoverageFrom: [
'src/**/*.{ts,tsx}',
'!src/**/*.d.ts',
'!src/**/index.ts',
],
};
export default config;// jest.setup.ts
import '@testing-library/jest-dom';Writing Tests — describe, it, expect, and Matchers
Every Jest test file is structured using three primary globals: describe for grouping related tests, it (or test) for individual test cases, and expect for assertions.
Basic Test Structure
// src/utils/math.ts
export function add(a: number, b: number): number {
return a + b;
}
export function divide(a: number, b: number): number {
if (b === 0) throw new Error('Division by zero');
return a / b;
}
// src/utils/math.test.ts
import { add, divide } from './math';
describe('add()', () => {
it('returns the sum of two positive numbers', () => {
expect(add(2, 3)).toBe(5);
});
it('handles negative numbers', () => {
expect(add(-1, -2)).toBe(-3);
});
it('returns the first argument when adding zero', () => {
expect(add(7, 0)).toBe(7);
});
});
describe('divide()', () => {
it('divides two numbers correctly', () => {
expect(divide(10, 2)).toBe(5);
});
it('throws on division by zero', () => {
expect(() => divide(10, 0)).toThrow('Division by zero');
});
});Essential Matchers Reference
| Matcher | Use Case | Example |
|---|---|---|
toBe(val) | Strict equality (===) | expect(1 + 1).toBe(2) |
toEqual(val) | Deep equality for objects/arrays | expect({a:1}).toEqual({a:1}) |
toStrictEqual(val) | Deep equality + checks undefined properties | expect(obj).toStrictEqual(expected) |
toBeTruthy() / toBeFalsy() | Truthy or falsy check | expect("hello").toBeTruthy() |
toBeNull() / toBeUndefined() | Checks for null or undefined | expect(null).toBeNull() |
toContain(item) | Array or string contains value | expect([1,2,3]).toContain(2) |
toHaveLength(n) | Array or string length | expect("abc").toHaveLength(3) |
toThrow(err?) | Function throws an error | expect(() => fn()).toThrow() |
toBeGreaterThan(n) | Number comparison | expect(5).toBeGreaterThan(3) |
toMatch(regex) | String matches regex or substring | expect("hello").toMatch(/ell/) |
toHaveProperty(path) | Object has a property | expect(obj).toHaveProperty('a.b') |
toBeCloseTo(n, digits?) | Floating point comparison | expect(0.1 + 0.2).toBeCloseTo(0.3) |
toHaveBeenCalled() | Mock function was called | expect(mockFn).toHaveBeenCalled() |
toHaveBeenCalledWith(...) | Mock called with specific args | expect(fn).toHaveBeenCalledWith(42, 'x') |
toHaveBeenCalledTimes(n) | Mock call count | expect(fn).toHaveBeenCalledTimes(3) |
Lifecycle Hooks
describe('Database service', () => {
let db: DatabaseService;
beforeAll(async () => {
// Runs once before all tests in this describe block
db = await DatabaseService.connect('sqlite::memory:');
});
afterAll(async () => {
// Runs once after all tests in this describe block
await db.disconnect();
});
beforeEach(async () => {
// Runs before EACH test — seed fresh data
await db.seed(testFixtures);
});
afterEach(async () => {
// Runs after EACH test — clean up
await db.truncate();
});
it('finds a user by id', async () => {
const user = await db.findUser(1);
expect(user).toHaveProperty('email');
});
});Mocking — jest.fn(), jest.mock(), and jest.spyOn()
Mocking replaces real dependencies with controlled substitutes, enabling tests to run in isolation without hitting databases, external APIs, or the filesystem. Jest provides three complementary tools for this.
jest.fn() — Standalone Mock Functions
// Create a mock function
const mockCallback = jest.fn();
// Call it like a real function
mockCallback('hello', 42);
// Inspect how it was called
expect(mockCallback).toHaveBeenCalledTimes(1);
expect(mockCallback).toHaveBeenCalledWith('hello', 42);
console.log(mockCallback.mock.calls); // [['hello', 42]]
console.log(mockCallback.mock.results); // [{ type: 'return', value: undefined }]
// Control return values
const mockFetch = jest.fn()
.mockReturnValueOnce({ data: 'first call' })
.mockReturnValueOnce({ data: 'second call' })
.mockReturnValue({ data: 'subsequent calls' });
// Async return values
const mockApi = jest.fn()
.mockResolvedValue({ status: 200, body: { id: 1 } });
const result = await mockApi();
expect(result.status).toBe(200);
// Reject with an error
const mockFailing = jest.fn()
.mockRejectedValue(new Error('Network error'));jest.mock() — Module-Level Mocking
// Auto-mock: all exports become jest.fn()
jest.mock('./emailService');
// Manual mock with factory function
jest.mock('./userRepository', () => ({
findById: jest.fn().mockResolvedValue({ id: 1, name: 'Alice' }),
save: jest.fn().mockResolvedValue(undefined),
}));
// Mock a third-party module
jest.mock('axios', () => ({
default: {
get: jest.fn().mockResolvedValue({ data: { users: [] } }),
post: jest.fn().mockResolvedValue({ data: { id: 42 } }),
},
}));
// Mock module with default export (ES module)
jest.mock('./config', () => ({
__esModule: true,
default: { apiUrl: 'http://test.local', timeout: 1000 },
}));
// Partial mock — keep some real implementations
jest.mock('./utils', () => ({
...jest.requireActual('./utils'), // keep real helpers
generateId: jest.fn().mockReturnValue('test-id-123'),
}));jest.spyOn() — Wrapping Existing Methods
import * as fs from 'fs';
describe('FileProcessor', () => {
afterEach(() => {
jest.restoreAllMocks(); // restore original implementations
});
it('reads a file and processes its content', () => {
// Spy on fs.readFileSync without replacing the entire module
const readSpy = jest.spyOn(fs, 'readFileSync')
.mockReturnValue('mocked file content');
const processor = new FileProcessor();
const result = processor.processFile('/path/to/file.txt');
expect(readSpy).toHaveBeenCalledWith('/path/to/file.txt', 'utf-8');
expect(result).toBe('MOCKED FILE CONTENT');
});
it('logs errors when file is missing', () => {
const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
jest.spyOn(fs, 'readFileSync').mockImplementation(() => {
throw new Error('ENOENT: no such file');
});
new FileProcessor().processFile('/missing.txt');
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining('ENOENT')
);
});
});Manual Mocks with __mocks__ Directory
// __mocks__/axios.ts (placed next to node_modules)
const axios = {
get: jest.fn(),
post: jest.fn(),
put: jest.fn(),
delete: jest.fn(),
create: jest.fn().mockReturnThis(),
interceptors: {
request: { use: jest.fn() },
response: { use: jest.fn() },
},
};
export default axios;
// In test files — jest.mock('axios') uses __mocks__/axios.ts automatically
jest.mock('axios');
import axios from 'axios';
test('fetches user data', async () => {
(axios.get as jest.Mock).mockResolvedValueOnce({
data: { id: 1, name: 'Alice' },
});
const user = await fetchUser(1);
expect(user.name).toBe('Alice');
});Testing React Components with React Testing Library
React Testing Library (RTL) builds on Jest's jsdom environment and provides utilities that encourage testing components the way users interact with them — by finding elements through accessible roles, labels, and text rather than CSS selectors or component internals.
Queries: Finding Elements the Right Way
| Query | When to Use | Throws if Missing? |
|---|---|---|
getByRole | Preferred — matches ARIA roles (button, textbox, heading…) | Yes |
getByLabelText | Form inputs with associated <label> | Yes |
getByPlaceholderText | Inputs with placeholder attribute | Yes |
getByText | Non-interactive elements: headings, paragraphs, spans | Yes |
getByDisplayValue | Current value of input/select/textarea | Yes |
getByAltText | Images with alt attribute | Yes |
getByTestId | Last resort — requires data-testid attribute | Yes |
queryBy* | Same as getBy* but returns null instead of throwing | No |
findBy* | Async — waits for element to appear (returns Promise) | Yes (async) |
getAllBy* / queryAllBy* / findAllBy* | Returns array of all matching elements | Varies |
Component Test Example
// components/LoginForm.tsx
import { useState } from 'react';
interface LoginFormProps {
onSubmit: (email: string, password: string) => Promise<void>;
}
export function LoginForm({ onSubmit }: LoginFormProps) {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError('');
try {
await onSubmit(email, password);
} catch (err) {
setError('Invalid credentials');
} finally {
setLoading(false);
}
};
return (
<form onSubmit={handleSubmit}>
<label htmlFor="email">Email</label>
<input id="email" type="email" value={email}
onChange={e => setEmail(e.target.value)} />
<label htmlFor="password">Password</label>
<input id="password" type="password" value={password}
onChange={e => setPassword(e.target.value)} />
{error && <p role="alert">{error}</p>}
<button type="submit" disabled={loading}>
{loading ? 'Signing in…' : 'Sign in'}
</button>
</form>
);
}// components/LoginForm.test.tsx
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { LoginForm } from './LoginForm';
describe('LoginForm', () => {
it('submits email and password when form is filled and submitted', async () => {
const user = userEvent.setup();
const mockOnSubmit = jest.fn().mockResolvedValue(undefined);
render(<LoginForm onSubmit={mockOnSubmit} />);
await user.type(screen.getByLabelText('Email'), 'alice@example.com');
await user.type(screen.getByLabelText('Password'), 'secret123');
await user.click(screen.getByRole('button', { name: 'Sign in' }));
await waitFor(() => {
expect(mockOnSubmit).toHaveBeenCalledWith('alice@example.com', 'secret123');
});
});
it('shows an error message when login fails', async () => {
const user = userEvent.setup();
const mockOnSubmit = jest.fn().mockRejectedValue(new Error('Unauthorized'));
render(<LoginForm onSubmit={mockOnSubmit} />);
await user.type(screen.getByLabelText('Email'), 'bad@example.com');
await user.type(screen.getByLabelText('Password'), 'wrong');
await user.click(screen.getByRole('button', { name: 'Sign in' }));
expect(await screen.findByRole('alert')).toHaveTextContent('Invalid credentials');
});
it('disables the submit button while loading', async () => {
const user = userEvent.setup();
// Never resolves so we can check the loading state
const mockOnSubmit = jest.fn().mockReturnValue(new Promise(() => {}));
render(<LoginForm onSubmit={mockOnSubmit} />);
await user.type(screen.getByLabelText('Email'), 'test@example.com');
await user.type(screen.getByLabelText('Password'), 'pass');
await user.click(screen.getByRole('button', { name: 'Sign in' }));
expect(screen.getByRole('button', { name: 'Signing in…' })).toBeDisabled();
});
});Async Testing — Promises, async/await, and Fake Timers
Modern JavaScript is heavily asynchronous. Jest handles async code through native async/await, promise matchers, and a powerful fake timer system.
async/await and Promise Matchers
// Testing a function that returns a Promise
async function fetchUser(id: number) {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
// Option 1: async/await (clearest syntax)
it('fetches a user successfully', async () => {
global.fetch = jest.fn().mockResolvedValue({
ok: true,
json: jest.fn().mockResolvedValue({ id: 1, name: 'Alice' }),
});
const user = await fetchUser(1);
expect(user.name).toBe('Alice');
});
// Option 2: resolves / rejects matchers
it('resolves with the correct user', () => {
global.fetch = jest.fn().mockResolvedValue({
ok: true,
json: jest.fn().mockResolvedValue({ id: 1, name: 'Alice' }),
});
return expect(fetchUser(1)).resolves.toMatchObject({ name: 'Alice' });
});
it('rejects when server returns 404', () => {
global.fetch = jest.fn().mockResolvedValue({ ok: false, status: 404 });
return expect(fetchUser(99)).rejects.toThrow('HTTP 404');
});Fake Timers for setTimeout and setInterval
// utils/debounce.ts
export function debounce<T extends (...args: unknown[]) => void>(
fn: T,
delay: number
): T {
let timer: ReturnType<typeof setTimeout>;
return ((...args: unknown[]) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
}) as T;
}
// utils/debounce.test.ts
import { debounce } from './debounce';
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
it('calls the function after the delay', () => {
const fn = jest.fn();
const debounced = debounce(fn, 300);
debounced();
expect(fn).not.toHaveBeenCalled(); // not yet
jest.advanceTimersByTime(300);
expect(fn).toHaveBeenCalledTimes(1);
});
it('only calls once if invoked multiple times within delay', () => {
const fn = jest.fn();
const debounced = debounce(fn, 300);
debounced();
debounced();
debounced();
jest.advanceTimersByTime(300);
expect(fn).toHaveBeenCalledTimes(1);
});
it('runs all pending timers with runAllTimers()', () => {
const fn = jest.fn();
const debounced = debounce(fn, 5000);
debounced();
jest.runAllTimers();
expect(fn).toHaveBeenCalledTimes(1);
});waitFor and findBy for DOM Updates
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { SearchBox } from './SearchBox';
it('shows search results after typing', async () => {
const user = userEvent.setup();
render(<SearchBox />);
await user.type(screen.getByRole('searchbox'), 'jest');
// waitFor keeps polling until assertion passes or times out (default 1000ms)
await waitFor(() => {
expect(screen.getByText('jest-circus')).toBeInTheDocument();
});
// findBy* is syntactic sugar for waitFor + getBy*
const result = await screen.findByText('jest-circus');
expect(result).toBeInTheDocument();
});Snapshot Testing — toMatchSnapshot and Inline Snapshots
Snapshot tests capture the rendered output of a component (or any serializable value) to a .snap file. On subsequent test runs, Jest compares the current output to the stored snapshot and fails if they differ, protecting against accidental regressions.
File Snapshots
// components/Badge.tsx
export function Badge({ label, variant }: { label: string; variant: 'success' | 'error' }) {
const bg = variant === 'success' ? '#dcfce7' : '#fee2e2';
const color = variant === 'success' ? '#166534' : '#991b1b';
return <span style={{ background: bg, color, padding: '2px 8px', borderRadius: '9999px' }}>{label}</span>;
}
// components/Badge.test.tsx
import { render } from '@testing-library/react';
import { Badge } from './Badge';
it('renders a success badge', () => {
const { asFragment } = render(<Badge label="Passing" variant="success" />);
expect(asFragment()).toMatchSnapshot();
// Creates __snapshots__/Badge.test.tsx.snap on first run
});
// To update after intentional UI changes:
// npx jest --updateSnapshot or npx jest -uInline Snapshots (Preferred for Small Values)
it('serializes a config object', () => {
const config = buildConfig({ env: 'test', debug: false });
expect(config).toMatchInlineSnapshot(`
{
"debug": false,
"env": "test",
"logLevel": "warn",
"retries": 3,
}
`);
// Jest writes the snapshot directly into the test file on first run
});Snapshot Best Practices
- Review every snapshot diff in code review — blindly running
-udefeats the purpose of snapshot testing - Prefer inline snapshots for small, focused values; they are easier to review in pull requests
- Commit snapshot files to version control alongside your test code
- Avoid snapshotting large component trees — they become brittle and obscure real regressions in noise. Target specific sub-trees or data structures instead
- Use
expect.any(String)or property matchers in snapshots to avoid failing on dynamic values like timestamps or UUIDs
// Property matchers — ignore dynamic values
it('creates a user with a generated id', () => {
const user = createUser({ name: 'Alice' });
expect(user).toMatchSnapshot({
id: expect.any(String), // ignore random UUID
createdAt: expect.any(Date), // ignore timestamp
});
// Only non-matched properties are snapshotted
});Code Coverage — Istanbul, Thresholds, and CI Integration
Run jest --coverage to generate a coverage report powered by Istanbul (NYC). Jest instruments your source code on the fly and tracks which lines, statements, branches, and functions are exercised during the test run.
Coverage Commands
# Generate coverage report (HTML, lcov, text)
npx jest --coverage
# Specify reporters
npx jest --coverage --coverageReporters=text --coverageReporters=html --coverageReporters=lcov
# Open HTML report
open coverage/index.html
# Fail if coverage drops below thresholds
npx jest --coverage --coverageThreshold='{"global":{"lines":80}}'Coverage Configuration in jest.config.ts
const config: Config = {
// ...
collectCoverageFrom: [
'src/**/*.{ts,tsx}',
'!src/**/*.d.ts',
'!src/**/*.stories.tsx', // exclude Storybook files
'!src/**/index.ts', // exclude barrel files
'!src/generated/**', // exclude generated code
],
coveragePathIgnorePatterns: [
'/node_modules/',
'/dist/',
'/__tests__/',
],
coverageReporters: ['text', 'lcov', 'html'],
coverageThreshold: {
global: {
branches: 75,
functions: 80,
lines: 80,
statements: 80,
},
// Per-file threshold (stricter for critical modules)
'./src/lib/auth.ts': {
branches: 90,
lines: 95,
},
},
};Coverage in GitHub Actions CI
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm test -- --coverage --ci --forceExit
env:
CI: true
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./coverage/lcov.infoUnderstanding the Coverage Report
| Metric | What it Measures | Typical Target |
|---|---|---|
| Statements | Percentage of executed statements | 80%+ |
| Branches | Percentage of if/else/ternary branches taken | 75%+ |
| Functions | Percentage of functions called at least once | 80%+ |
| Lines | Percentage of source lines executed | 80%+ |
Testing Custom Hooks with renderHook and act
Custom hooks often encapsulate complex stateful logic. React Testing Library's renderHook utility lets you test hooks in isolation without needing a full component wrapper. Wrap state-changing calls in act() to flush React's update queue.
// hooks/useCounter.ts
import { useState, useCallback } from 'react';
export function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);
const increment = useCallback(() => setCount(c => c + 1), []);
const decrement = useCallback(() => setCount(c => c - 1), []);
const reset = useCallback(() => setCount(initialValue), [initialValue]);
return { count, increment, decrement, reset };
}
// hooks/useCounter.test.ts
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';
describe('useCounter', () => {
it('initializes with the provided value', () => {
const { result } = renderHook(() => useCounter(10));
expect(result.current.count).toBe(10);
});
it('increments the counter', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
it('decrements the counter', () => {
const { result } = renderHook(() => useCounter(5));
act(() => {
result.current.decrement();
});
expect(result.current.count).toBe(4);
});
it('resets to initial value', () => {
const { result } = renderHook(() => useCounter(3));
act(() => { result.current.increment(); });
act(() => { result.current.increment(); });
act(() => { result.current.reset(); });
expect(result.current.count).toBe(3);
});
});Testing Hooks with Context Providers
// hooks/useAuth.ts
import { useContext } from 'react';
import { AuthContext } from '../context/AuthContext';
export function useAuth() {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error('useAuth must be used within an AuthProvider');
return ctx;
}
// hooks/useAuth.test.tsx
import { renderHook } from '@testing-library/react';
import { AuthProvider } from '../context/AuthContext';
import { useAuth } from './useAuth';
const wrapper = ({ children }: { children: React.ReactNode }) => (
<AuthProvider initialUser={{ id: '1', name: 'Alice', role: 'admin' }}>
{children}
</AuthProvider>
);
it('returns the current user from AuthContext', () => {
const { result } = renderHook(() => useAuth(), { wrapper });
expect(result.current.user).toMatchObject({ name: 'Alice', role: 'admin' });
});
it('throws when used outside AuthProvider', () => {
// renderHook without wrapper — no provider present
expect(() => renderHook(() => useAuth())).toThrow(
'useAuth must be used within an AuthProvider'
);
});Jest vs Vitest vs Mocha vs Jasmine — Framework Comparison
Choosing a test framework depends on your project's bundler, runtime, and team preferences. Here is a direct comparison of the four most popular JavaScript testing frameworks in 2025.
| Feature | Jest | Vitest | Mocha | Jasmine |
|---|---|---|---|---|
| Maintained by | Meta (Facebook) | Vitest team / Anthony Fu | Community | Pivotal / Community |
| Initial release | 2014 | 2021 | 2011 | 2010 |
| Zero config | Yes | Yes (Vite projects) | No — needs assertion lib + runner | Yes |
| TypeScript support | Via ts-jest or Babel | Native (Vite) | Via ts-node / Babel | Via types + Babel |
| Built-in assertions | Yes (expect) | Yes (expect — Jest-compatible) | No (use Chai, etc.) | Yes (expect) |
| Built-in mocking | Yes (jest.fn, jest.mock) | Yes (vi.fn, vi.mock) | No (use Sinon) | Yes (spyOn) |
| Snapshot testing | Yes | Yes (Jest-compatible) | Plugin needed | Plugin needed |
| Code coverage | Yes (Istanbul) | Yes (V8 or Istanbul) | Plugin (nyc) | Plugin needed |
| Fake timers | Yes | Yes | Via Sinon | Yes |
| Watch mode | Yes | Yes (HMR-powered) | Plugin (nodemon) | Via CLI |
| jsdom support | Yes (built-in) | Yes (via jsdom pool) | Via jsdom package | Via jsdom package |
| ESM support | Experimental | Native | Yes (v10+) | Limited |
| Speed | Good (parallel workers) | Fastest (Vite HMR) | Moderate | Good |
| Best for | React / CRA / large apps | Vite / Vue / Svelte | Custom setups / Node.js | Angular / legacy |
| GitHub stars (2025) | ~44K | ~13K | ~23K | ~16K |
Migration from Jest to Vitest
Vitest is API-compatible with Jest for most use cases. The migration steps are minimal:
# 1. Install Vitest
npm install --save-dev vitest @vitest/coverage-v8 jsdom
# 2. Update vite.config.ts
import { defineConfig } from 'vite';
export default defineConfig({
test: {
globals: true, // no need to import describe/it/expect
environment: 'jsdom',
setupFiles: ['./vitest.setup.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'html', 'lcov'],
thresholds: { lines: 80, functions: 80, branches: 75 },
},
},
});
# 3. Replace jest.fn() with vi.fn(), jest.mock() with vi.mock()
# Most code requires no changes — the API is intentionally compatibleAdvanced Patterns — Test Factories, Custom Matchers, and Projects
Test Data Factories
// test/factories/userFactory.ts
import { v4 as uuid } from 'uuid';
interface User {
id: string;
name: string;
email: string;
role: 'admin' | 'user';
createdAt: Date;
}
export function createUser(overrides: Partial<User> = {}): User {
return {
id: uuid(),
name: 'Test User',
email: 'test@example.com',
role: 'user',
createdAt: new Date('2024-01-01'),
...overrides,
};
}
// Usage in tests
const admin = createUser({ role: 'admin', name: 'Alice Admin' });
const users = Array.from({ length: 10 }, (_, i) => createUser({ name: `User ${i}` }));Custom Matchers with expect.extend()
// test/matchers/customMatchers.ts
expect.extend({
toBeValidEmail(received: string) {
const emailRegex = /^[^s@]+@[^s@]+.[^s@]+$/;
const pass = emailRegex.test(received);
return {
pass,
message: () =>
pass
? `expected ${received} not to be a valid email`
: `expected ${received} to be a valid email`,
};
},
toBeWithinRange(received: number, floor: number, ceiling: number) {
const pass = received >= floor && received <= ceiling;
return {
pass,
message: () =>
`expected ${received} ${pass ? 'not ' : ''}to be within range ${floor} - ${ceiling}`,
};
},
});
// Extend TypeScript types
declare global {
namespace jest {
interface Matchers<R> {
toBeValidEmail(): R;
toBeWithinRange(floor: number, ceiling: number): R;
}
}
}
// Usage
expect('user@example.com').toBeValidEmail();
expect(75).toBeWithinRange(0, 100);Jest Projects Configuration (Monorepo)
// jest.config.ts (root of monorepo)
const config: Config = {
projects: [
{
displayName: 'frontend',
testEnvironment: 'jsdom',
testMatch: ['<rootDir>/packages/frontend/**/*.test.tsx'],
setupFilesAfterFramework: ['<rootDir>/jest.setup.ts'],
},
{
displayName: 'backend',
testEnvironment: 'node',
testMatch: ['<rootDir>/packages/backend/**/*.test.ts'],
},
{
displayName: 'shared',
testEnvironment: 'node',
testMatch: ['<rootDir>/packages/shared/**/*.test.ts'],
},
],
coverageThreshold: {
global: { lines: 80 },
},
};Debugging and Troubleshooting Common Jest Issues
Debugging with VS Code
// .vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Jest: Debug current file",
"program": "${workspaceFolder}/node_modules/.bin/jest",
"args": [
"${relativeFile}",
"--no-coverage",
"--runInBand",
"--watchAll=false"
],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen"
}
]
}Common Issues and Fixes
| Issue | Cause | Fix |
|---|---|---|
| SyntaxError: Cannot use import statement | Jest runs in CommonJS by default; ESM modules not transformed | Add transformIgnorePatterns override or use Babel/ts-jest |
| Cannot find module "X" from test file | moduleNameMapper not configured for path aliases | Add alias mapping to moduleNameMapper in jest.config |
| ReferenceError: document is not defined | testEnvironment defaults to "node" for non-browser tests | Set testEnvironment: "jsdom" or add @jest-environment jsdom docblock |
| act() warning: An update was not wrapped in act() | State update triggered outside React's rendering cycle | Wrap state-triggering calls in act() or use userEvent |
| Snapshot tests fail every time | Dynamic values (Date, UUID) stored in snapshots | Use expect.any(Date) property matchers to ignore dynamic values |
| Tests hang and do not exit | Open handles: DB connections, HTTP servers not closed | Use --forceExit or close connections in afterAll |
| Mock not resetting between tests | clearMocks not configured, shared module-level mock state | Set clearMocks: true in config or call jest.clearAllMocks() in afterEach |
Frequently Asked Questions
How do I mock an ES module with Jest?
Use jest.mock() at the top of the test file. For named exports, provide a factory function. For default exports, set __esModule: true in the factory. For projects using native ESM (without Babel), use jest.unstable_mockModule() with --experimental-vm-modules.
How do I set up Jest in a CI/CD pipeline?
Add a test:ci script: jest --ci --coverage --forceExit. The --ci flag disables interactive mode and fails on outdated snapshots. Set coverageThreshold in jest.config.js to enforce minimum coverage. Use the jest-junit reporter to emit JUnit XML for test result parsing in GitHub Actions or Jenkins.
What is the difference between jest.fn(), jest.mock(), and jest.spyOn()?
jest.fn() creates a standalone mock function for dependency injection. jest.mock() replaces an entire module for all imports within a test file. jest.spyOn() wraps an existing method on an object, allowing tracking while optionally keeping the original implementation. Use mockRestore() to undo spies in afterEach.
How do I achieve test isolation between Jest tests?
Configure clearMocks: true (reset call counts) or resetMocks: true (also reset return values) in jest.config.js. Use beforeEach to set up fresh state. Jest runs each test file in a separate Node.js worker by default, providing automatic file-level isolation. For database tests, wrap each test in a transaction and roll it back.
How do I test async code with fake timers?
Call jest.useFakeTimers() in beforeEach and jest.useRealTimers() in afterEach. Advance time with jest.advanceTimersByTime(ms) or flush all pending timers with jest.runAllTimers(). In React component tests, combine with act() to flush state updates.
When should I update snapshots?
Only update snapshots when you intentionally changed the output. Run jest --updateSnapshot (or -u) and review every diff carefully. Never batch-update all snapshots without reviewing changes — this defeats the regression-detection purpose. Prefer inline snapshots for small values that are easier to review in pull requests.
How do I improve slow Jest test suites?
Key optimizations: (1) enable cache: true in config, (2) stub heavy modules (CSS, images) via moduleNameMapper, (3) use jest.mock() to avoid real I/O, (4) set maxWorkers to 50% of CPU cores in CI, (5) limit collectCoverageFrom to source files only, and (6) consider switching to Vitest for Vite-based projects where native ESM and HMR make reruns significantly faster.
What is the best way to test API calls in React components?
Use Mock Service Worker (MSW) to intercept HTTP requests at the network level using a service worker or Node.js interceptor. MSW works transparently with any HTTP library (fetch, axios, ky) and makes tests realistic without depending on a real server. Set up MSW handlers in jest.setup.ts, define request handlers for test scenarios, and use server.use() for per-test overrides. Alternatively, use jest.mock('axios') for simpler scenarios where full network interception is unnecessary.
Related Developer Tools
When writing tests you often need to generate test data, validate JSON schemas, or format output. DevToolBox provides several free tools to streamline your testing workflow:
- JSON Formatter — Validate and format JSON responses returned by mocked API calls
- Regex Tester — Build and test regular expressions used in custom Jest matchers
- UUID Generator — Generate test UUIDs for use in test fixtures and factories
- Base64 Encoder/Decoder — Encode and decode test payloads and authorization headers
- Hash Generator — Generate SHA-256 hashes for testing cryptographic utilities