Jest Cheatsheet¶
Jest - Delightful JavaScript Testing
Jest is a delightful JavaScript testing framework with a focus on simplicity. It works out of the box for most JavaScript projects and provides features like snapshot testing, built-in test runner, assertion library, and powerful mocking capabilities.
Table of Contents¶
- Installation
- Getting Started
- Basic Testing
- Matchers
- Async Testing
- Mocking
- Snapshot Testing
- Configuration
- React Testing
- Code Coverage
- Advanced Features
- Testing Patterns
- Performance
- Integration
- Troubleshooting
- Best Practices
Installation¶
Basic Installation¶
# Install Jest
npm install --save-dev jest
# Install with TypeScript support
npm install --save-dev jest @types/jest ts-jest
# Install with Babel support
npm install --save-dev jest babel-jest @babel/core @babel/preset-env
# Global installation (not recommended)
npm install -g jest
React Projects¶
# Create React App (Jest included)
npx create-react-app my-app
cd my-app
# Manual React setup
npm install --save-dev jest @testing-library/react @testing-library/jest-dom
# React Native
npm install --save-dev jest react-test-renderer
Package.json Configuration¶
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"test:ci": "jest --ci --coverage --watchAll=false"
},
"devDependencies": {
"jest": "^29.0.0",
"@testing-library/jest-dom": "^5.16.0"
}
}
Project Structure¶
my-project/
├── src/
│ ├── components/
│ │ ├── Button.js
│ │ └── Button.test.js
│ ├── utils/
│ │ ├── math.js
│ │ └── math.test.js
│ └── __tests__/
│ └── integration.test.js
├── __mocks__/
│ └── fileMock.js
├── jest.config.js
└── package.json
Getting Started¶
First Test¶
// math.js
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
export function multiply(a, b) {
return a * b;
}
export function divide(a, b) {
if (b === 0) {
throw new Error('Division by zero');
}
return a / b;
}
// math.test.js
import { add, subtract, multiply, divide } from './math';
describe('Math functions', () => {
test('adds 1 + 2 to equal 3', () => {
expect(add(1, 2)).toBe(3);
});
test('subtracts 5 - 3 to equal 2', () => {
expect(subtract(5, 3)).toBe(2);
});
test('multiplies 3 * 4 to equal 12', () => {
expect(multiply(3, 4)).toBe(12);
});
test('divides 8 / 2 to equal 4', () => {
expect(divide(8, 2)).toBe(4);
});
test('throws error when dividing by zero', () => {
expect(() => divide(10, 0)).toThrow('Division by zero');
});
});
Running Tests¶
# Run all tests
npm test
# Run tests in watch mode
npm run test:watch
# Run specific test file
npm test math.test.js
# Run tests matching pattern
npm test -- --testNamePattern="add"
# Run tests with coverage
npm run test:coverage
# Run tests in CI mode
npm run test:ci
Test Organization¶
// Grouping tests with describe
describe('Calculator', () => {
describe('Addition', () => {
test('should add positive numbers', () => {
expect(add(2, 3)).toBe(5);
});
test('should add negative numbers', () => {
expect(add(-2, -3)).toBe(-5);
});
test('should add zero', () => {
expect(add(5, 0)).toBe(5);
});
});
describe('Division', () => {
test('should divide positive numbers', () => {
expect(divide(10, 2)).toBe(5);
});
test('should handle division by zero', () => {
expect(() => divide(10, 0)).toThrow();
});
});
});
Basic Testing¶
Test Structure¶
// Basic test structure
test('description of what is being tested', () => {
// Arrange - set up test data
const input = 'hello';
const expected = 'HELLO';
// Act - execute the function
const result = input.toUpperCase();
// Assert - verify the result
expect(result).toBe(expected);
});
// Alternative syntax
it('should convert string to uppercase', () => {
expect('hello'.toUpperCase()).toBe('HELLO');
});
Setup and Teardown¶
describe('Database tests', () => {
let database;
// Run before all tests in this describe block
beforeAll(async () => {
database = await connectToDatabase();
});
// Run after all tests in this describe block
afterAll(async () => {
await database.close();
});
// Run before each test
beforeEach(() => {
database.clear();
});
// Run after each test
afterEach(() => {
database.cleanup();
});
test('should save user', async () => {
const user = { name: 'John', email: 'john@example.com' };
await database.save(user);
const savedUser = await database.findByEmail('john@example.com');
expect(savedUser).toEqual(user);
});
});
Skipping and Focusing Tests¶
// Skip tests
describe.skip('Skipped test suite', () => {
test('this will not run', () => {
expect(true).toBe(false);
});
});
test.skip('skipped test', () => {
expect(true).toBe(false);
});
// Focus on specific tests (only these will run)
describe.only('Focused test suite', () => {
test('this will run', () => {
expect(true).toBe(true);
});
});
test.only('focused test', () => {
expect(true).toBe(true);
});
// Conditional tests
const runIntegrationTests = process.env.NODE_ENV === 'test';
(runIntegrationTests ? describe : describe.skip)('Integration tests', () => {
test('should integrate with external service', () => {
// Integration test code
});
});
Parameterized Tests¶
// Test with multiple inputs
describe('isPrime function', () => {
test.each([
[2, true],
[3, true],
[4, false],
[5, true],
[6, false],
[7, true],
[8, false],
[9, false],
[10, false],
[11, true],
])('isPrime(%i) should return %s', (input, expected) => {
expect(isPrime(input)).toBe(expected);
});
});
// Test with objects
describe('user validation', () => {
test.each([
{ name: 'John', email: 'john@example.com', valid: true },
{ name: '', email: 'john@example.com', valid: false },
{ name: 'John', email: 'invalid-email', valid: false },
{ name: 'John', email: '', valid: false },
])('validateUser($name, $email) should return $valid', ({ name, email, valid }) => {
expect(validateUser({ name, email })).toBe(valid);
});
});
Matchers¶
Basic Matchers¶
describe('Basic matchers', () => {
test('equality matchers', () => {
expect(2 + 2).toBe(4); // Exact equality (Object.is)
expect({ name: 'John' }).toEqual({ name: 'John' }); // Deep equality
expect({ name: 'John' }).not.toBe({ name: 'John' }); // Different objects
});
test('truthiness matchers', () => {
expect(true).toBeTruthy();
expect(false).toBeFalsy();
expect(null).toBeNull();
expect(undefined).toBeUndefined();
expect('hello').toBeDefined();
});
test('number matchers', () => {
expect(2 + 2).toBeGreaterThan(3);
expect(2 + 2).toBeGreaterThanOrEqual(4);
expect(2 + 2).toBeLessThan(5);
expect(2 + 2).toBeLessThanOrEqual(4);
expect(0.1 + 0.2).toBeCloseTo(0.3); // Floating point
});
});
String Matchers¶
describe('String matchers', () => {
test('string matching', () => {
expect('Hello World').toMatch(/World/);
expect('Hello World').toMatch('World');
expect('user@example.com').toMatch(/^[\w\.-]+@[\w\.-]+\.\w+$/);
expect('Hello World').toContain('World');
expect('Hello World').not.toContain('Goodbye');
expect('Hello World').toHaveLength(11);
expect('').toHaveLength(0);
});
});
Array and Object Matchers¶
describe('Array and object matchers', () => {
test('array matchers', () => {
const fruits = ['apple', 'banana', 'orange'];
expect(fruits).toContain('banana');
expect(fruits).toHaveLength(3);
expect(fruits).toEqual(['apple', 'banana', 'orange']);
expect(fruits).toEqual(expect.arrayContaining(['apple', 'banana']));
});
test('object matchers', () => {
const user = {
id: 1,
name: 'John',
email: 'john@example.com',
profile: {
age: 30,
city: 'New York'
}
};
expect(user).toHaveProperty('name');
expect(user).toHaveProperty('profile.age', 30);
expect(user).toMatchObject({
name: 'John',
email: 'john@example.com'
});
expect(user).toEqual(expect.objectContaining({
name: 'John',
id: expect.any(Number)
}));
});
});
Exception Matchers¶
describe('Exception matchers', () => {
test('function throws', () => {
const throwError = () => {
throw new Error('Something went wrong');
};
expect(throwError).toThrow();
expect(throwError).toThrow('Something went wrong');
expect(throwError).toThrow(/wrong/);
expect(throwError).toThrow(Error);
});
test('async function throws', async () => {
const asyncThrowError = async () => {
throw new Error('Async error');
};
await expect(asyncThrowError()).rejects.toThrow('Async error');
});
});
Custom Matchers¶
// Custom matcher
expect.extend({
toBeWithinRange(received, floor, ceiling) {
const pass = received >= floor && received <= ceiling;
if (pass) {
return {
message: () =>
`expected ${received} not to be within range ${floor} - ${ceiling}`,
pass: true,
};
} else {
return {
message: () =>
`expected ${received} to be within range ${floor} - ${ceiling}`,
pass: false,
};
}
},
});
// Usage
test('numeric ranges', () => {
expect(100).toBeWithinRange(90, 110);
expect(101).not.toBeWithinRange(0, 100);
});
// Async custom matcher
expect.extend({
async toBeValidUser(received) {
const isValid = await validateUserAsync(received);
if (isValid) {
return {
message: () => `expected ${received} not to be a valid user`,
pass: true,
};
} else {
return {
message: () => `expected ${received} to be a valid user`,
pass: false,
};
}
},
});
Async Testing¶
Promises¶
describe('Promise testing', () => {
// Return promise
test('async function returns promise', () => {
return fetchUser(1).then(user => {
expect(user.name).toBe('John');
});
});
// Async/await
test('async function with async/await', async () => {
const user = await fetchUser(1);
expect(user.name).toBe('John');
});
// Testing rejections
test('async function rejects', async () => {
await expect(fetchUser(-1)).rejects.toThrow('User not found');
});
// Testing both resolve and reject
test('promise resolves', () => {
return expect(fetchUser(1)).resolves.toEqual({
id: 1,
name: 'John'
});
});
});
Callbacks¶
describe('Callback testing', () => {
// Callback with done
test('callback function', (done) => {
function callback(data) {
try {
expect(data).toBe('callback data');
done();
} catch (error) {
done(error);
}
}
fetchDataCallback(callback);
});
// Testing error callbacks
test('error callback', (done) => {
function errorCallback(error) {
try {
expect(error.message).toBe('Something went wrong');
done();
} catch (err) {
done(err);
}
}
fetchDataWithError(errorCallback);
});
});
Timers and Delays¶
describe('Timer testing', () => {
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
test('delayed function', () => {
const callback = jest.fn();
setTimeout(callback, 1000);
// Fast-forward time
jest.advanceTimersByTime(1000);
expect(callback).toHaveBeenCalled();
});
test('interval function', () => {
const callback = jest.fn();
setInterval(callback, 1000);
// Fast-forward multiple intervals
jest.advanceTimersByTime(3000);
expect(callback).toHaveBeenCalledTimes(3);
});
test('run all timers', () => {
const callback = jest.fn();
setTimeout(callback, 1000);
setTimeout(callback, 2000);
jest.runAllTimers();
expect(callback).toHaveBeenCalledTimes(2);
});
});
Mocking¶
Function Mocking¶
describe('Function mocking', () => {
test('mock function', () => {
const mockFn = jest.fn();
mockFn('arg1', 'arg2');
mockFn('arg3');
expect(mockFn).toHaveBeenCalledTimes(2);
expect(mockFn).toHaveBeenCalledWith('arg1', 'arg2');
expect(mockFn).toHaveBeenLastCalledWith('arg3');
});
test('mock return values', () => {
const mockFn = jest.fn();
mockFn.mockReturnValue(42);
expect(mockFn()).toBe(42);
mockFn.mockReturnValueOnce(1).mockReturnValueOnce(2);
expect(mockFn()).toBe(1);
expect(mockFn()).toBe(2);
expect(mockFn()).toBe(42); // Default return value
});
test('mock implementation', () => {
const mockFn = jest.fn((x, y) => x + y);
expect(mockFn(1, 2)).toBe(3);
expect(mockFn).toHaveBeenCalledWith(1, 2);
});
});
Module Mocking¶
// userService.js
import axios from 'axios';
export async function getUser(id) {
const response = await axios.get(`/api/users/${id}`);
return response.data;
}
export async function createUser(userData) {
const response = await axios.post('/api/users', userData);
return response.data;
}
// userService.test.js
import axios from 'axios';
import { getUser, createUser } from './userService';
// Mock the entire axios module
jest.mock('axios');
const mockedAxios = axios as jest.Mocked<typeof axios>;
describe('User Service', () => {
beforeEach(() => {
jest.clearAllMocks();
});
test('should fetch user', async () => {
const userData = { id: 1, name: 'John' };
mockedAxios.get.mockResolvedValue({ data: userData });
const user = await getUser(1);
expect(user).toEqual(userData);
expect(mockedAxios.get).toHaveBeenCalledWith('/api/users/1');
});
test('should create user', async () => {
const newUser = { name: 'Jane', email: 'jane@example.com' };
const createdUser = { id: 2, ...newUser };
mockedAxios.post.mockResolvedValue({ data: createdUser });
const user = await createUser(newUser);
expect(user).toEqual(createdUser);
expect(mockedAxios.post).toHaveBeenCalledWith('/api/users', newUser);
});
});
Partial Mocking¶
// math.js
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
export const multiply = (a, b) => a * b;
// calculator.js
import * as math from './math';
export function calculate(operation, a, b) {
switch (operation) {
case 'add':
return math.add(a, b);
case 'subtract':
return math.subtract(a, b);
case 'multiply':
return math.multiply(a, b);
default:
throw new Error('Unknown operation');
}
}
// calculator.test.js
import { calculate } from './calculator';
import * as math from './math';
// Partial mock - only mock specific functions
jest.mock('./math', () => ({
...jest.requireActual('./math'),
add: jest.fn(),
}));
const mockedMath = math as jest.Mocked<typeof math>;
describe('Calculator', () => {
test('should use mocked add function', () => {
mockedMath.add.mockReturnValue(100);
const result = calculate('add', 2, 3);
expect(result).toBe(100);
expect(mockedMath.add).toHaveBeenCalledWith(2, 3);
});
test('should use real subtract function', () => {
const result = calculate('subtract', 5, 3);
expect(result).toBe(2); // Real implementation
});
});
Spy Functions¶
describe('Spy functions', () => {
test('spy on object method', () => {
const user = {
getName: () => 'John',
setName: (name) => { this.name = name; }
};
const spy = jest.spyOn(user, 'getName');
const name = user.getName();
expect(spy).toHaveBeenCalled();
expect(name).toBe('John');
spy.mockRestore(); // Restore original implementation
});
test('spy on console.log', () => {
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
console.log('Hello, World!');
expect(consoleSpy).toHaveBeenCalledWith('Hello, World!');
consoleSpy.mockRestore();
});
});
Mock Files¶
// __mocks__/fs.js
const fs = jest.createMockFromModule('fs');
let mockFiles = Object.create(null);
function __setMockFiles(newMockFiles) {
mockFiles = Object.create(null);
for (const file in newMockFiles) {
const dir = path.dirname(file);
if (!mockFiles[dir]) {
mockFiles[dir] = [];
}
mockFiles[dir].push(path.basename(file));
}
}
function readFileSync(filePath) {
return mockFiles[filePath] || '';
}
fs.__setMockFiles = __setMockFiles;
fs.readFileSync = readFileSync;
module.exports = fs;
// fileReader.test.js
import fs from 'fs';
import { readConfigFile } from './fileReader';
jest.mock('fs');
describe('File Reader', () => {
test('should read config file', () => {
fs.__setMockFiles({
'/config/app.json': '{"name": "MyApp"}'
});
const config = readConfigFile('/config/app.json');
expect(config).toEqual({ name: 'MyApp' });
});
});
Snapshot Testing¶
Basic Snapshots¶
// Button.js
import React from 'react';
export function Button({ children, onClick, variant = 'primary' }) {
return (
<button
className={`btn btn-${variant}`}
onClick={onClick}
>
{children}
</button>
);
}
// Button.test.js
import React from 'react';
import { render } from '@testing-library/react';
import { Button } from './Button';
describe('Button', () => {
test('should match snapshot', () => {
const { container } = render(
<Button variant="primary">Click me</Button>
);
expect(container.firstChild).toMatchSnapshot();
});
test('should match snapshot with different variant', () => {
const { container } = render(
<Button variant="secondary">Cancel</Button>
);
expect(container.firstChild).toMatchSnapshot();
});
});
Inline Snapshots¶
describe('Inline snapshots', () => {
test('should match inline snapshot', () => {
const user = {
id: 1,
name: 'John',
createdAt: new Date('2023-01-01')
};
expect(user).toMatchInlineSnapshot(`
Object {
"createdAt": 2023-01-01T00:00:00.000Z,
"id": 1,
"name": "John",
}
`);
});
});
Custom Snapshot Serializers¶
// Custom serializer for Date objects
expect.addSnapshotSerializer({
test: (val) => val instanceof Date,
print: (val) => `Date("${val.toISOString()}")`,
});
// Custom serializer for React elements
expect.addSnapshotSerializer({
test: (val) => val && val.$$typeof === Symbol.for('react.element'),
print: (val, serialize) => {
const { type, props } = val;
const { children, ...restProps } = props;
return `<${type}${Object.keys(restProps).length ? ' ' + serialize(restProps) : ''}>${
children ? serialize(children) : ''
}</${type}>`;
},
});
Property Matchers¶
describe('Property matchers', () => {
test('should match snapshot with dynamic values', () => {
const user = {
id: Math.random(),
name: 'John',
createdAt: new Date(),
profile: {
lastLogin: new Date(),
sessionId: 'abc123'
}
};
expect(user).toMatchSnapshot({
id: expect.any(Number),
createdAt: expect.any(Date),
profile: {
lastLogin: expect.any(Date),
sessionId: expect.any(String)
}
});
});
});
Configuration¶
Jest Configuration File¶
// jest.config.js
module.exports = {
// Test environment
testEnvironment: 'jsdom', // 'node' | 'jsdom'
// Test file patterns
testMatch: [
'**/__tests__/**/*.(js|jsx|ts|tsx)',
'**/*.(test|spec).(js|jsx|ts|tsx)'
],
// Setup files
setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],
// Module name mapping
moduleNameMapping: {
'^@/(.*)$': '<rootDir>/src/$1',
'\\.(css|less|scss|sass)$': 'identity-obj-proxy',
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2)$': '<rootDir>/__mocks__/fileMock.js'
},
// Transform files
transform: {
'^.+\\.(js|jsx|ts|tsx)$': 'babel-jest',
'^.+\\.css$': '<rootDir>/config/jest/cssTransform.js'
},
// Coverage
collectCoverageFrom: [
'src/**/*.{js,jsx,ts,tsx}',
'!src/**/*.d.ts',
'!src/index.js',
'!src/serviceWorker.js'
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
},
// Ignore patterns
testPathIgnorePatterns: [
'/node_modules/',
'/build/',
'/dist/'
],
// Module directories
moduleDirectories: ['node_modules', '<rootDir>/src'],
// Globals
globals: {
'ts-jest': {
tsconfig: 'tsconfig.json'
}
}
};
TypeScript Configuration¶
// jest.config.js for TypeScript
module.exports = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
moduleNameMapping: {
'^@/(.*)$': '<rootDir>/src/$1'
},
setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'],
transform: {
'^.+\\.tsx?$': 'ts-jest'
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],
globals: {
'ts-jest': {
tsconfig: {
jsx: 'react-jsx'
}
}
}
};
Setup Files¶
// src/setupTests.js
import '@testing-library/jest-dom';
// Mock IntersectionObserver
global.IntersectionObserver = class IntersectionObserver {
constructor() {}
disconnect() {}
observe() {}
unobserve() {}
};
// Mock matchMedia
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation(query => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(),
removeListener: jest.fn(),
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
});
// Global test utilities
global.testUtils = {
createMockUser: () => ({
id: 1,
name: 'Test User',
email: 'test@example.com'
})
};
Environment Variables¶
// jest.config.js
module.exports = {
setupFiles: ['<rootDir>/config/jest/setEnvVars.js'],
};
// config/jest/setEnvVars.js
process.env.NODE_ENV = 'test';
process.env.API_URL = 'http://localhost:3001';
process.env.FEATURE_FLAG_NEW_UI = 'true';
React Testing¶
Component Testing¶
// UserProfile.js
import React, { useState, useEffect } from 'react';
export function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function fetchUser() {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error('User not found');
}
const userData = await response.json();
setUser(userData);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
fetchUser();
}, [userId]);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!user) return <div>No user found</div>;
return (
<div>
<h1>{user.name}</h1>
<p>Email: {user.email}</p>
<p>Age: {user.age}</p>
</div>
);
}
// UserProfile.test.js
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import { UserProfile } from './UserProfile';
// Mock fetch
global.fetch = jest.fn();
describe('UserProfile', () => {
beforeEach(() => {
fetch.mockClear();
});
test('should display loading state', () => {
fetch.mockImplementation(() => new Promise(() => {})); // Never resolves
render(<UserProfile userId={1} />);
expect(screen.getByText('Loading...')).toBeInTheDocument();
});
test('should display user data', async () => {
const mockUser = {
id: 1,
name: 'John Doe',
email: 'john@example.com',
age: 30
};
fetch.mockResolvedValueOnce({
ok: true,
json: async () => mockUser
});
render(<UserProfile userId={1} />);
await waitFor(() => {
expect(screen.getByText('John Doe')).toBeInTheDocument();
});
expect(screen.getByText('Email: john@example.com')).toBeInTheDocument();
expect(screen.getByText('Age: 30')).toBeInTheDocument();
});
test('should display error message', async () => {
fetch.mockRejectedValueOnce(new Error('Network error'));
render(<UserProfile userId={1} />);
await waitFor(() => {
expect(screen.getByText('Error: Network error')).toBeInTheDocument();
});
});
});
Event Testing¶
// Counter.js
import React, { useState } from 'react';
export function Counter({ initialValue = 0, onCountChange }) {
const [count, setCount] = useState(initialValue);
const increment = () => {
const newCount = count + 1;
setCount(newCount);
onCountChange?.(newCount);
};
const decrement = () => {
const newCount = count - 1;
setCount(newCount);
onCountChange?.(newCount);
};
const reset = () => {
setCount(initialValue);
onCountChange?.(initialValue);
};
return (
<div>
<span data-testid="count">{count}</span>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
<button onClick={reset}>Reset</button>
</div>
);
}
// Counter.test.js
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Counter } from './Counter';
describe('Counter', () => {
test('should increment count', async () => {
const user = userEvent.setup();
render(<Counter />);
const incrementButton = screen.getByText('+');
await user.click(incrementButton);
expect(screen.getByTestId('count')).toHaveTextContent('1');
});
test('should call onCountChange when count changes', async () => {
const user = userEvent.setup();
const onCountChange = jest.fn();
render(<Counter onCountChange={onCountChange} />);
const incrementButton = screen.getByText('+');
await user.click(incrementButton);
expect(onCountChange).toHaveBeenCalledWith(1);
});
test('should reset to initial value', async () => {
const user = userEvent.setup();
render(<Counter initialValue={5} />);
const incrementButton = screen.getByText('+');
const resetButton = screen.getByText('Reset');
await user.click(incrementButton);
expect(screen.getByTestId('count')).toHaveTextContent('6');
await user.click(resetButton);
expect(screen.getByTestId('count')).toHaveTextContent('5');
});
});
Hook Testing¶
// useCounter.js
import { useState, useCallback } from 'react';
export function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);
const increment = useCallback(() => {
setCount(prev => prev + 1);
}, []);
const decrement = useCallback(() => {
setCount(prev => prev - 1);
}, []);
const reset = useCallback(() => {
setCount(initialValue);
}, [initialValue]);
return { count, increment, decrement, reset };
}
// useCounter.test.js
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';
describe('useCounter', () => {
test('should initialize with default value', () => {
const { result } = renderHook(() => useCounter());
expect(result.current.count).toBe(0);
});
test('should initialize with custom value', () => {
const { result } = renderHook(() => useCounter(10));
expect(result.current.count).toBe(10);
});
test('should increment count', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
test('should decrement count', () => {
const { result } = renderHook(() => useCounter(5));
act(() => {
result.current.decrement();
});
expect(result.current.count).toBe(4);
});
test('should reset count', () => {
const { result } = renderHook(() => useCounter(10));
act(() => {
result.current.increment();
result.current.increment();
});
expect(result.current.count).toBe(12);
act(() => {
result.current.reset();
});
expect(result.current.count).toBe(10);
});
});
Code Coverage¶
Coverage Configuration¶
// jest.config.js
module.exports = {
collectCoverage: true,
collectCoverageFrom: [
'src/**/*.{js,jsx,ts,tsx}',
'!src/**/*.d.ts',
'!src/index.js',
'!src/reportWebVitals.js',
'!src/**/*.stories.{js,jsx,ts,tsx}',
'!src/**/*.test.{js,jsx,ts,tsx}'
],
coverageDirectory: 'coverage',
coverageReporters: [
'text',
'lcov',
'html',
'json'
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
},
'./src/components/': {
branches: 90,
functions: 90,
lines: 90,
statements: 90
}
}
};
Coverage Scripts¶
{
"scripts": {
"test:coverage": "jest --coverage",
"test:coverage:watch": "jest --coverage --watchAll",
"test:coverage:ci": "jest --coverage --ci --watchAll=false",
"coverage:open": "open coverage/lcov-report/index.html"
}
}
Ignoring Coverage¶
// Ignore specific lines
function myFunction() {
/* istanbul ignore next */
if (process.env.NODE_ENV === 'development') {
console.log('Development mode');
}
return 'result';
}
// Ignore entire function
/* istanbul ignore next */
function debugFunction() {
console.log('Debug info');
}
// Ignore else branch
function processValue(value) {
if (value) {
return value.toUpperCase();
}
/* istanbul ignore else */
else {
return '';
}
}
Advanced Features¶
Custom Test Environment¶
// jest-environment-custom.js
const { TestEnvironment } = require('jest-environment-jsdom');
class CustomTestEnvironment extends TestEnvironment {
constructor(config, context) {
super(config, context);
// Add custom globals
this.global.customGlobal = 'test-value';
}
async setup() {
await super.setup();
// Custom setup logic
this.global.mockDatabase = {
users: [],
addUser: (user) => this.global.mockDatabase.users.push(user),
getUser: (id) => this.global.mockDatabase.users.find(u => u.id === id)
};
}
async teardown() {
// Custom teardown logic
this.global.mockDatabase = null;
await super.teardown();
}
}
module.exports = CustomTestEnvironment;
// jest.config.js
module.exports = {
testEnvironment: './jest-environment-custom.js'
};
Custom Reporters¶
// custom-reporter.js
class CustomReporter {
constructor(globalConfig, options) {
this._globalConfig = globalConfig;
this._options = options;
}
onRunStart(results, options) {
console.log('🚀 Starting test run...');
}
onTestStart(test) {
console.log(`▶️ Running ${test.path}`);
}
onTestResult(test, testResult, aggregatedResult) {
if (testResult.numFailingTests > 0) {
console.log(`❌ ${test.path} - ${testResult.numFailingTests} failed`);
} else {
console.log(`✅ ${test.path} - all tests passed`);
}
}
onRunComplete(contexts, results) {
console.log(`🏁 Test run complete: ${results.numPassedTests} passed, ${results.numFailedTests} failed`);
}
}
module.exports = CustomReporter;
// jest.config.js
module.exports = {
reporters: [
'default',
['./custom-reporter.js', { option1: 'value1' }]
]
};
Global Setup and Teardown¶
// jest.config.js
module.exports = {
globalSetup: './config/jest/globalSetup.js',
globalTeardown: './config/jest/globalTeardown.js'
};
// config/jest/globalSetup.js
module.exports = async () => {
console.log('🔧 Global setup');
// Start test database
global.__TEST_DB__ = await startTestDatabase();
// Start mock server
global.__MOCK_SERVER__ = await startMockServer();
};
// config/jest/globalTeardown.js
module.exports = async () => {
console.log('🧹 Global teardown');
// Stop test database
await global.__TEST_DB__.stop();
// Stop mock server
await global.__MOCK_SERVER__.stop();
};
Watch Plugins¶
// watch-plugin-custom.js
class CustomWatchPlugin {
constructor({ stdin, stdout, config, testPathPattern }) {
this._stdin = stdin;
this._stdout = stdout;
this._config = config;
this._testPathPattern = testPathPattern;
}
apply(jestHooks) {
jestHooks.onFileChange(({ projects }) => {
console.log('📁 Files changed, running tests...');
});
}
getUsageInfo() {
return {
key: 'c',
prompt: 'clear console'
};
}
run(globalConfig, updateConfigAndRun) {
console.clear();
return Promise.resolve();
}
}
module.exports = CustomWatchPlugin;
// jest.config.js
module.exports = {
watchPlugins: [
'jest-watch-typeahead/filename',
'jest-watch-typeahead/testname',
'./watch-plugin-custom.js'
]
};
Testing Patterns¶
Page Object Model¶
// pageObjects/LoginPage.js
export class LoginPage {
constructor(page) {
this.page = page;
}
get emailInput() {
return this.page.getByLabelText(/email/i);
}
get passwordInput() {
return this.page.getByLabelText(/password/i);
}
get submitButton() {
return this.page.getByRole('button', { name: /login/i });
}
get errorMessage() {
return this.page.getByTestId('error-message');
}
async login(email, password) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
async expectErrorMessage(message) {
await expect(this.errorMessage).toHaveText(message);
}
}
// Login.test.js
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { LoginPage } from './pageObjects/LoginPage';
import { Login } from './Login';
describe('Login', () => {
test('should display error for invalid credentials', async () => {
const user = userEvent.setup();
render(<Login />);
const loginPage = new LoginPage(screen);
await loginPage.login('invalid@email.com', 'wrongpassword');
await loginPage.expectErrorMessage('Invalid credentials');
});
});
Test Factories¶
// factories/userFactory.js
export const createUser = (overrides = {}) => ({
id: Math.floor(Math.random() * 1000),
name: 'John Doe',
email: 'john@example.com',
age: 30,
isActive: true,
createdAt: new Date(),
...overrides
});
export const createUsers = (count, overrides = {}) =>
Array.from({ length: count }, (_, index) =>
createUser({ id: index + 1, ...overrides })
);
// Usage in tests
import { createUser, createUsers } from './factories/userFactory';
describe('User Service', () => {
test('should process user data', () => {
const user = createUser({ name: 'Jane Doe', age: 25 });
const result = processUser(user);
expect(result.displayName).toBe('Jane Doe (25)');
});
test('should handle multiple users', () => {
const users = createUsers(5, { isActive: true });
const activeUsers = filterActiveUsers(users);
expect(activeUsers).toHaveLength(5);
});
});
Test Utilities¶
// testUtils/renderWithProviders.js
import React from 'react';
import { render } from '@testing-library/react';
import { BrowserRouter } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from 'react-query';
import { ThemeProvider } from 'styled-components';
import { theme } from '../src/theme';
export function renderWithProviders(
ui,
{
initialEntries = ['/'],
queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false }
}
}),
...renderOptions
} = {}
) {
function Wrapper({ children }) {
return (
<BrowserRouter>
<QueryClientProvider client={queryClient}>
<ThemeProvider theme={theme}>
{children}
</ThemeProvider>
</QueryClientProvider>
</BrowserRouter>
);
}
return render(ui, { wrapper: Wrapper, ...renderOptions });
}
// Usage
import { renderWithProviders } from './testUtils/renderWithProviders';
test('should render with all providers', () => {
renderWithProviders(<MyComponent />);
// Test implementation
});
Custom Matchers for Domain Logic¶
// matchers/userMatchers.js
expect.extend({
toBeValidUser(received) {
const pass =
received &&
typeof received.id === 'number' &&
typeof received.name === 'string' &&
received.name.length > 0 &&
typeof received.email === 'string' &&
received.email.includes('@');
if (pass) {
return {
message: () => `expected ${JSON.stringify(received)} not to be a valid user`,
pass: true,
};
} else {
return {
message: () => `expected ${JSON.stringify(received)} to be a valid user`,
pass: false,
};
}
},
toHavePermission(received, permission) {
const pass = received.permissions && received.permissions.includes(permission);
if (pass) {
return {
message: () => `expected user not to have permission "${permission}"`,
pass: true,
};
} else {
return {
message: () => `expected user to have permission "${permission}"`,
pass: false,
};
}
}
});
// Usage
test('should validate user object', () => {
const user = { id: 1, name: 'John', email: 'john@example.com' };
expect(user).toBeValidUser();
});
test('should check user permissions', () => {
const user = { permissions: ['read', 'write'] };
expect(user).toHavePermission('read');
});
Performance¶
Test Performance Optimization¶
// jest.config.js
module.exports = {
// Parallel execution
maxWorkers: '50%', // Use 50% of available cores
// Cache
cache: true,
cacheDirectory: '/tmp/jest_cache',
// Faster test discovery
testPathIgnorePatterns: [
'/node_modules/',
'/build/',
'/coverage/'
],
// Optimize transforms
transform: {
'^.+\\.(js|jsx)$': ['babel-jest', { cacheDirectory: true }]
},
// Reduce overhead
clearMocks: true,
restoreMocks: true,
// Faster test environment
testEnvironment: 'jsdom',
testEnvironmentOptions: {
url: 'http://localhost'
}
};
Selective Test Running¶
# Run only changed files
npm test -- --onlyChanged
# Run related tests
npm test -- --findRelatedTests src/components/Button.js
# Run tests matching pattern
npm test -- --testNamePattern="should render"
# Run specific test suites
npm test -- --testPathPattern="components"
# Bail on first failure
npm test -- --bail
# Run tests in band (no parallel)
npm test -- --runInBand
Memory Optimization¶
// jest.config.js
module.exports = {
// Limit memory usage
workerIdleMemoryLimit: '512MB',
// Clear modules between tests
clearMocks: true,
resetMocks: true,
restoreMocks: true,
// Optimize setup
setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],
// Reduce test overhead
testEnvironment: 'jsdom',
// Optimize coverage collection
collectCoverageFrom: [
'src/**/*.{js,jsx}',
'!src/**/*.test.{js,jsx}',
'!src/**/*.stories.{js,jsx}'
]
};
// Cleanup in tests
afterEach(() => {
jest.clearAllMocks();
cleanup(); // From @testing-library/react
});
Integration¶
CI/CD Integration¶
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16, 18, 20]
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test -- --ci --coverage --watchAll=false
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: ./coverage/lcov.info
Docker Integration¶
# Dockerfile.test
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
CMD ["npm", "test", "--", "--ci", "--coverage", "--watchAll=false"]
IDE Integration¶
// .vscode/settings.json
{
"jest.jestCommandLine": "npm test --",
"jest.autoRun": {
"watch": true,
"onStartup": ["all-tests"]
},
"jest.showCoverageOnLoad": true,
"jest.coverageFormatter": "DefaultFormatter",
"jest.debugMode": true
}
Troubleshooting¶
Common Issues¶
# Clear Jest cache
npx jest --clearCache
# Debug Jest configuration
npx jest --showConfig
# Run with debug output
DEBUG=jest* npm test
# Memory issues
NODE_OPTIONS="--max-old-space-size=4096" npm test
# Permission issues
sudo chown -R $(whoami) node_modules/.cache
Debugging Tests¶
// Debug specific test
test.only('debug this test', () => {
console.log('Debug info');
debugger; // Use with --inspect-brk
expect(true).toBe(true);
});
// Debug with VS Code
// Add to launch.json
{
"type": "node",
"request": "launch",
"name": "Jest Debug",
"program": "${workspaceFolder}/node_modules/.bin/jest",
"args": ["--runInBand", "--no-cache"],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen"
}
Mock Issues¶
// Reset mocks between tests
beforeEach(() => {
jest.clearAllMocks();
jest.resetAllMocks();
jest.restoreAllMocks();
});
// Mock implementation issues
const mockFn = jest.fn();
mockFn.mockImplementation(() => {
throw new Error('Mock error');
});
// Module mock issues
jest.mock('./module', () => ({
__esModule: true,
default: jest.fn(),
namedExport: jest.fn()
}));
Best Practices¶
Test Organization¶
- Descriptive Names: Use clear, descriptive test names
- Arrange-Act-Assert: Follow the AAA pattern
- Single Responsibility: One assertion per test when possible
- Test Structure: Group related tests with
describe
blocks
Test Quality¶
- Edge Cases: Test boundary conditions and edge cases
- Error Handling: Test error scenarios and exceptions
- Async Code: Properly test asynchronous operations
- Mocking Strategy: Mock external dependencies appropriately
Performance¶
- Selective Running: Use watch mode and selective test running
- Parallel Execution: Leverage Jest's parallel execution
- Cache Optimization: Use Jest's caching effectively
- Memory Management: Clean up resources and mocks
Maintenance¶
- Regular Updates: Keep Jest and testing libraries updated
- Coverage Goals: Set and maintain appropriate coverage thresholds
- Test Documentation: Document complex test scenarios
- Refactoring: Refactor tests along with production code
Summary¶
Jest is a comprehensive JavaScript testing framework that provides:
- Zero Configuration: Works out of the box for most projects
- Powerful Matchers: Extensive assertion library with custom matcher support
- Mocking Capabilities: Built-in mocking for functions, modules, and timers
- Snapshot Testing: Visual regression testing for UI components
- Code Coverage: Built-in coverage reporting with threshold enforcement
- Parallel Execution: Fast test execution with worker processes
- Watch Mode: Interactive development with automatic test re-running
- Extensive Ecosystem: Rich ecosystem of plugins and integrations
Jest excels at testing React applications, Node.js backends, and general JavaScript code. Its focus on developer experience, comprehensive feature set, and excellent documentation make it the go-to choice for JavaScript testing in modern development workflows.