| name | jest |
| description | Tests JavaScript and TypeScript applications with Jest test runner including mocking, snapshot testing, and code coverage. Use when setting up testing, writing unit tests, or when user mentions Jest, test runner, or JavaScript testing. |
Jest
Delightful JavaScript testing framework with zero configuration and rich mocking capabilities.
Quick Start
# Install
npm install -D jest @types/jest
# With TypeScript
npm install -D jest ts-jest @types/jest
npx ts-jest config:init
# Or with Babel
npm install -D jest @babel/preset-env @babel/preset-typescript
Configuration
jest.config.ts
import type { Config } from 'jest';
const config: Config = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/src'],
testMatch: ['**/__tests__/**/*.ts', '**/*.test.ts', '**/*.spec.ts'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
},
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
collectCoverageFrom: [
'src/**/*.{ts,tsx}',
'!src/**/*.d.ts',
'!src/**/__tests__/**',
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
clearMocks: true,
verbose: true,
};
export default config;
For React (with Testing Library)
import type { Config } from 'jest';
const config: Config = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
moduleNameMapper: {
'\\.(css|less|scss|sass)$': 'identity-obj-proxy',
'\\.(jpg|jpeg|png|gif|webp|svg)$': '<rootDir>/__mocks__/fileMock.js',
},
transform: {
'^.+\\.tsx?$': ['ts-jest', { tsconfig: 'tsconfig.jest.json' }],
},
};
export default config;
jest.setup.ts
import '@testing-library/jest-dom';
// Global mocks
global.fetch = jest.fn();
// Extend expect
expect.extend({
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}`,
};
},
});
declare global {
namespace jest {
interface Matchers<R> {
toBeWithinRange(floor: number, ceiling: number): R;
}
}
}
Basic Tests
Test Structure
import { describe, it, expect, test, beforeEach, afterEach } from '@jest/globals';
describe('Calculator', () => {
let calculator: Calculator;
beforeEach(() => {
calculator = new Calculator();
});
afterEach(() => {
// Cleanup
});
describe('add', () => {
it('should add two positive numbers', () => {
expect(calculator.add(1, 2)).toBe(3);
});
it('should handle negative numbers', () => {
expect(calculator.add(-1, -2)).toBe(-3);
});
test.each([
[1, 2, 3],
[0, 0, 0],
[-1, 1, 0],
[100, 200, 300],
])('add(%i, %i) should return %i', (a, b, expected) => {
expect(calculator.add(a, b)).toBe(expected);
});
});
describe('divide', () => {
it('should throw when dividing by zero', () => {
expect(() => calculator.divide(10, 0)).toThrow('Division by zero');
});
});
});
Common Matchers
// Equality
expect(value).toBe(expected); // ===
expect(value).toEqual(expected); // Deep equality
expect(value).toStrictEqual(expected); // Deep + undefined props
// Truthiness
expect(value).toBeNull();
expect(value).toBeUndefined();
expect(value).toBeDefined();
expect(value).toBeTruthy();
expect(value).toBeFalsy();
// Numbers
expect(value).toBeGreaterThan(3);
expect(value).toBeGreaterThanOrEqual(3);
expect(value).toBeLessThan(5);
expect(value).toBeCloseTo(0.3, 5); // Floating point
// Strings
expect(string).toMatch(/pattern/);
expect(string).toContain('substring');
expect(string).toHaveLength(5);
// Arrays
expect(array).toContain(item);
expect(array).toContainEqual(object);
expect(array).toHaveLength(3);
// Objects
expect(object).toHaveProperty('key');
expect(object).toHaveProperty('nested.key', value);
expect(object).toMatchObject({ key: value });
// Exceptions
expect(() => fn()).toThrow();
expect(() => fn()).toThrow('message');
expect(() => fn()).toThrow(ErrorType);
// Negation
expect(value).not.toBe(other);
// Asymmetric matchers
expect(value).toEqual(expect.any(Number));
expect(value).toEqual(expect.stringContaining('sub'));
expect(value).toEqual(expect.arrayContaining([1, 2]));
expect(value).toEqual(expect.objectContaining({ key: value }));
Async Testing
Promises
// Return promise
test('fetches user data', () => {
return fetchUser(1).then((user) => {
expect(user.name).toBe('Alice');
});
});
// Async/await
test('fetches user data', async () => {
const user = await fetchUser(1);
expect(user.name).toBe('Alice');
});
// Resolves/rejects
test('resolves to user', async () => {
await expect(fetchUser(1)).resolves.toEqual({ name: 'Alice' });
});
test('rejects with error', async () => {
await expect(fetchUser(-1)).rejects.toThrow('User not found');
});
Callbacks
test('callback with done', (done) => {
fetchDataWithCallback((error, data) => {
try {
expect(error).toBeNull();
expect(data).toEqual({ success: true });
done();
} catch (e) {
done(e);
}
});
});
Timers
jest.useFakeTimers();
test('delays execution', () => {
const callback = jest.fn();
delayedCall(callback, 1000);
expect(callback).not.toHaveBeenCalled();
jest.advanceTimersByTime(1000);
expect(callback).toHaveBeenCalledTimes(1);
});
test('with modern timers', () => {
jest.useFakeTimers({ advanceTimers: true });
const callback = jest.fn();
setTimeout(callback, 1000);
jest.runAllTimers();
expect(callback).toHaveBeenCalled();
});
afterEach(() => {
jest.useRealTimers();
});
Mocking
Function Mocks
// Create mock function
const mockFn = jest.fn();
// With implementation
const mockAdd = jest.fn((a, b) => a + b);
// Mock return values
mockFn.mockReturnValue(42);
mockFn.mockReturnValueOnce(1).mockReturnValueOnce(2);
mockFn.mockResolvedValue({ data: 'value' });
mockFn.mockRejectedValue(new Error('Failed'));
// Assertions
expect(mockFn).toHaveBeenCalled();
expect(mockFn).toHaveBeenCalledTimes(2);
expect(mockFn).toHaveBeenCalledWith('arg1', 'arg2');
expect(mockFn).toHaveBeenLastCalledWith('lastArg');
expect(mockFn).toHaveBeenNthCalledWith(1, 'firstCallArg');
expect(mockFn).toHaveReturnedWith(42);
Module Mocks
// __mocks__/axios.ts
const axios = {
get: jest.fn(),
post: jest.fn(),
create: jest.fn(() => axios),
};
export default axios;
// In test file
jest.mock('axios');
import axios from 'axios';
const mockedAxios = axios as jest.Mocked<typeof axios>;
test('fetches data', async () => {
mockedAxios.get.mockResolvedValue({ data: { users: [] } });
const result = await fetchUsers();
expect(mockedAxios.get).toHaveBeenCalledWith('/api/users');
expect(result).toEqual({ users: [] });
});
Partial Mocks
// Mock specific exports
jest.mock('./utils', () => ({
...jest.requireActual('./utils'),
expensiveOperation: jest.fn(),
}));
// Mock with factory
jest.mock('./database', () => {
return {
connect: jest.fn().mockResolvedValue(true),
query: jest.fn(),
};
});
Spy on Methods
const video = {
play() {
return true;
},
};
test('plays video', () => {
const spy = jest.spyOn(video, 'play');
video.play();
expect(spy).toHaveBeenCalled();
spy.mockRestore();
});
// Spy on prototype
jest.spyOn(Date.prototype, 'toISOString').mockReturnValue('2024-01-01');
Manual Mocks
// __mocks__/fs.ts
const fs = jest.createMockFromModule('fs') as typeof import('fs');
let mockFiles: Record<string, string> = {};
fs.readFileSync = jest.fn((path: string) => {
if (path in mockFiles) {
return mockFiles[path];
}
throw new Error(`ENOENT: ${path}`);
});
(fs as any).__setMockFiles = (files: Record<string, string>) => {
mockFiles = files;
};
export default fs;
Snapshot Testing
// Basic snapshot
test('renders correctly', () => {
const tree = renderer.create(<Button label="Click me" />).toJSON();
expect(tree).toMatchSnapshot();
});
// Inline snapshot
test('formats date', () => {
expect(formatDate(new Date('2024-01-15'))).toMatchInlineSnapshot(
`"January 15, 2024"`
);
});
// Property matchers for dynamic values
test('creates user', () => {
const user = createUser('Alice');
expect(user).toMatchSnapshot({
id: expect.any(String),
createdAt: expect.any(Date),
});
});
// Update snapshots: jest --updateSnapshot
Code Coverage
# Run with coverage
jest --coverage
# Coverage report options
jest --coverage --coverageReporters="text" --coverageReporters="html"
// jest.config.ts coverage options
{
collectCoverage: true,
coverageDirectory: 'coverage',
coverageReporters: ['text', 'lcov', 'html'],
collectCoverageFrom: [
'src/**/*.{ts,tsx}',
'!src/**/*.d.ts',
'!src/index.ts',
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
'./src/utils/': {
branches: 100,
statements: 100,
},
},
}
Testing React Components
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
test('submits form with user data', async () => {
const handleSubmit = jest.fn();
const user = userEvent.setup();
render(<LoginForm onSubmit={handleSubmit} />);
await user.type(screen.getByLabelText(/email/i), 'test@example.com');
await user.type(screen.getByLabelText(/password/i), 'password123');
await user.click(screen.getByRole('button', { name: /submit/i }));
expect(handleSubmit).toHaveBeenCalledWith({
email: 'test@example.com',
password: 'password123',
});
});
test('shows error on invalid input', async () => {
render(<LoginForm />);
fireEvent.click(screen.getByRole('button', { name: /submit/i }));
await waitFor(() => {
expect(screen.getByText(/email is required/i)).toBeInTheDocument();
});
});
test('fetches and displays users', async () => {
const mockUsers = [{ id: 1, name: 'Alice' }];
global.fetch = jest.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve(mockUsers),
});
render(<UserList />);
expect(screen.getByText(/loading/i)).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText('Alice')).toBeInTheDocument();
});
});
Running Tests
# Run all tests
jest
# Watch mode
jest --watch
# Run specific file
jest path/to/test.spec.ts
# Run tests matching pattern
jest --testNamePattern="should add"
# Run only changed files
jest --onlyChanged
# Parallel execution
jest --maxWorkers=4
# Debug mode
node --inspect-brk node_modules/.bin/jest --runInBand
Reference Files
- mocking-patterns.md - Advanced mocking techniques
- performance.md - Optimizing test performance