⏳
Loading cheatsheet...
Unit testing, mocks, async assertions, setup patterns and coverage practices with Jest.
# ── Installation ──
# For Node.js projects
npm install --save-dev jest
npm install --save-dev @types/jest # TypeScript support
# For React projects
npm install --save-dev jest @testing-library/react @testing-library/jest-dom
npm install --save-dev @testing-library/user-event
# For Next.js (comes pre-configured)
npx create-next-app@latest # includes Jest setup
# Run tests
npx jest
npx jest --watch # watch mode
npx jest --coverage # with coverage
npx jest --verbose
npx jest --testPathPattern="user" # run matching files
npx jest --testNamePattern="should login" # run matching tests// ── Basic Test Structure ──
// Files: *.test.js, *.spec.js, or in __tests__/ folders
import { add, multiply } from './math';
describe('Math utilities', () => {
// Runs once before all tests in this describe
beforeAll(() => {
console.log('Starting math tests');
});
// Runs before each test
beforeEach(() => {
// reset state
});
afterAll(() => {
console.log('All math tests done');
});
afterEach(() => {
// cleanup after each test
});
test('add should add two numbers', () => {
expect(add(2, 3)).toBe(5);
});
test('multiply should multiply two numbers', () => {
expect(multiply(2, 3)).toBe(6);
});
// Shorthand: it() is an alias for test()
it('should handle negative numbers', () => {
expect(add(-1, 1)).toBe(0);
});
// Skip a test
it.skip('skip this test', () => {
// This won't run
});
// Mark as only test to run
it.only('only this test runs', () => {
expect(true).toBe(true);
});
});{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"test:ci": "jest --ci --coverage --reporters=default --reporters=jest-junit"
}
}| Hook | When | Use Case |
|---|---|---|
| beforeAll() | Once before all tests | Expensive setup |
| beforeEach() | Before each test | Reset state, seed data |
| afterEach() | After each test | Cleanup, reset mocks |
| afterAll() | Once after all tests | Close connections |
| Pattern | Example | Scope |
|---|---|---|
| *.test.js | math.test.js | Test file |
| *.spec.js | math.spec.js | Test file |
| *.test.ts | math.test.ts | TypeScript test |
| __tests__/ | __tests__/math.js | Test directory |
| testPathPattern | --testPathPattern=api | CLI filter |
*.test.js, *.spec.js, or inside __tests__/ folders are automatically collected. Use describe and it/test for organization.// ── expect(value) & Matchers ──
// ── Exact Equality ──
expect(2 + 2).toBe(4); // strict === equality
expect({ a: 1 }).toEqual({ a: 1 }); // deep recursive equality
expect(1).not.toBe(2); // negation
// ── Truthiness ──
expect(null).toBeNull();
expect(undefined).toBeUndefined();
expect(undefined).not.toBeDefined();
expect(true).toBeTruthy();
expect(false).toBeFalsy();
expect(0).toBeFalsy();
expect('').toBeFalsy();
expect(NaN).toBeFalsy();
// ── Numbers ──
expect(value).toBeGreaterThan(3);
expect(value).toBeGreaterThanOrEqual(4);
expect(value).toBeLessThan(10);
expect(value).toBeLessThanOrEqual(9);
expect(value).toBeCloseTo(0.3); // float precision
expect(0.1 + 0.2).toBeCloseTo(0.3); // avoids 0.30000000000000004
expect(NaN).toBeNaN();
expect(Infinity).toBeFinite(); // fails! use .not.toBeFinite()
expect(isFinite(Infinity)).toBe(false);
// ── Strings ──
expect('hello world').toMatch(/hello/);
expect('hello world').toMatch('world');
expect('hello').toContain('ell');
// ── Arrays & Iterables ──
expect([1, 2, 3]).toContain(2);
expect([1, 2, 3]).toContainEqual({ a: 1 }); // structural match
expect([1, 2, 3]).toEqual(expect.arrayContaining([2, 3]));
expect([1, 2, 3]).toHaveLength(3);
// ── Objects ──
expect({ a: 1, b: 2 }).toHaveProperty('a');
expect({ a: 1, b: 2 }).toHaveProperty('a', 1);
expect({ a: 1, b: 2 }).toMatchObject({ a: 1 });
expect({ a: 1, b: 2, c: 3 }).toEqual(expect.objectContaining({ a: 1 }));// ── Exception & Function Testing ──
// Test that function throws
function throwError() {
throw new Error('Something went wrong');
}
test('should throw an error', () => {
expect(throwError).toThrow();
expect(throwError).toThrow('Something went wrong');
expect(throwError).toThrow(/went wrong/);
expect(throwError).toThrow(Error);
});
// Async error testing
async function asyncError() {
throw new Error('Async error');
}
test('should reject with error', async () => {
await expect(asyncError()).rejects.toThrow('Async error');
await expect(asyncError()).rejects.toThrow(Error);
});
// Test that function does NOT throw
function safeFunc() {
return 42;
}
test('should not throw', () => {
expect(safeFunc).not.toThrow();
});
// Partial matching on thrown error
class AppError extends Error {
constructor(message, code) {
super(message);
this.code = code;
}
}
test('should throw AppError with code', () => {
expect(() => {
throw new AppError('Not found', 404);
}).toThrow(expect.objectContaining({
message: 'Not found',
code: 404,
}));
});| Matcher | Use Case |
|---|---|
| toBe(x) | Strict equality (===) |
| toEqual(x) | Deep equality (objects, arrays) |
| toBeNull() | Value is null |
| toBeUndefined() | Value is undefined |
| toBeTruthy() | Value is truthy |
| toBeFalsy() | Value is falsy |
| toBeGreaterThan(n) | Number > n |
| toBeCloseTo(n) | Float approximation |
| toMatch(/reg/) | String regex match |
| toContain(x) | Array/iterable includes x |
| toHaveLength(n) | Array/object length |
| toHaveProperty(k) | Object has property |
| toThrow() | Function throws error |
| Pattern | Description |
|---|---|
| not.toBe(x) | Not strictly equal |
| not.toEqual(x) | Not deeply equal |
| not.toContain(x) | Array excludes x |
| not.toThrow() | Does not throw |
| not.toBeNull() | Is not null |
| not.toBeUndefined() | Is not undefined |
| not.toHaveProperty(k) | Missing property |
| not.arrayContaining([x]) | Excludes elements |
| not.objectContaining({k:v}) | Excludes properties |
toBeCloseTo() for floating-point comparisons. Never use toBe() with floats — binary precision causes 0.1 + 0.2 !== 0.3.rejects for async error tests: await expect(promise).rejects.toThrow(). This is cleaner than try/catch blocks.// ── Asymmetric Matchers ──
// expect.objectContaining — partial object match
expect({
id: 1,
name: 'Alice',
email: 'alice@test.com',
role: 'admin',
}).toEqual(
expect.objectContaining({
name: 'Alice',
role: 'admin',
})
);
// expect.arrayContaining — partial array match
expect([1, 2, 3, 4, 5]).toEqual(
expect.arrayContaining([2, 4])
);
// expect.stringContaining — substring match
expect('hello world foo bar').toEqual(
expect.stringContaining('world')
);
// expect.stringMatching — regex match in strings
expect('user-12345').toEqual(
expect.stringMatching(/^user-\d+$/)
);
// expect.any — match any instance of type
expect({ id: 1, name: 'Alice', created: new Date() }).toEqual({
id: expect.any(Number),
name: expect.any(String),
created: expect.any(Date),
});
// expect.anything — match any non-null value
expect({ id: 1, data: null }).toEqual({
id: expect.anything(), // passes
data: null, // expect.anything() would FAIL for null
});
// expect.extend — custom matchers
expect.extend({
toBeWithinRange(received, floor, ceiling) {
const pass = received >= floor && received <= ceiling;
return {
pass,
message: () =>
`expected ${received} to be within ${floor}..${ceiling}`,
};
},
});
test('custom matcher', () => {
expect(42).toBeWithinRange(0, 100);
expect(150).not.toBeWithinRange(0, 100);
});// ── @testing-library/jest-dom Matchers ──
// Enhances expect() with DOM-specific matchers
import '@testing-library/jest-dom';
test('DOM element assertions', () => {
render(<MyComponent />);
// Visibility
expect(screen.getByText('Hello')).toBeInTheDocument();
expect(screen.getByText('Hidden')).not.toBeVisible();
expect(screen.getByText('Gone')).not.toBeInTheDocument();
// State
expect(screen.getByRole('button')).toBeEnabled();
expect(screen.getByRole('button')).toBeDisabled();
expect(screen.getByRole('checkbox')).toBeChecked();
expect(screen.getByRole('textbox')).toBeEmpty();
// Content
expect(screen.getByText(/welcome/i)).toHaveTextContent('Welcome back!');
expect(screen.getByTestId('card')).toHaveAttribute('data-testid', 'card');
expect(screen.getByRole('img')).toHaveAttribute('alt', 'Profile');
expect(screen.getByRole('link')).toHaveStyle({ color: 'blue' });
expect(screen.getByRole('form')).toHaveClass('form-container');
// Form state
expect(screen.getByLabelText('Email')).toHaveValue('test@test.com');
expect(screen.getByPlaceholderText('Search')).toHaveDisplayValue('');
// Focus
expect(screen.getByRole('textbox')).toHaveFocus();
// Custom data attribute
expect(screen.getByTestId('status')).toHaveAttribute('data-status', 'active');
});| Matcher | Matches |
|---|---|
| expect.any(Constructor) | Any instance of type |
| expect.anything() | Any non-null/non-undefined |
| expect.arrayContaining(arr) | Superset of arr |
| expect.objectContaining(obj) | Superset of obj |
| expect.stringContaining(str) | String contains str |
| expect.stringMatching(reg) | String matches regex |
| expect.not.arrayContaining() | Excludes elements |
| expect.not.objectContaining() | Excludes properties |
| Matcher | Description |
|---|---|
| toBeInTheDocument() | Element in DOM |
| toBeVisible() | Element visible |
| toBeEnabled() | Form element enabled |
| toBeDisabled() | Form element disabled |
| toBeChecked() | Checkbox/radio checked |
| toHaveTextContent() | Contains text |
| toHaveValue() | Form input value |
| toHaveAttribute() | Has HTML attribute |
| toHaveClass() | Has CSS class |
| toHaveStyle() | Has inline style |
| toHaveFocus() | Element is focused |
| toBeEmptyDOMElement() | No children |
@testing-library/jest-dom and add import '@testing-library/jest-dom' (or configure in Jest setupFilesAfterSetup) for DOM-specific matchers. They make component testing much more readable.// ── Mocking Functions ──
// jest.fn() — creates a mock function
const mockCallback = jest.fn(x => x * 2);
[1, 2, 3].forEach(mockCallback);
expect(mockCallback.mock.calls.length).toBe(3);
expect(mockCallback.mock.calls[0][0]).toBe(1);
expect(mockCallback.mock.results[0].value).toBe(2);
// jest.fn() with implementation
const mockAdd = jest.fn((a, b) => a + b);
expect(mockAdd(1, 2)).toBe(3);
mockAdd.mockReturnValueOnce(10);
mockAdd.mockReturnValueOnce(20);
expect(mockAdd(1, 2)).toBe(10);
expect(mockAdd(1, 2)).toBe(20);
expect(mockAdd(1, 2)).toBe(3); // back to original
// jest.spyOn — spy on existing methods
const utils = {
formatDate: (d) => d.toISOString(),
parseJSON: (s) => JSON.parse(s),
};
test('spy on formatDate', () => {
const spy = jest.spyOn(utils, 'formatDate');
spy.mockReturnValue('2024-01-15');
const result = utils.formatDate(new Date());
expect(result).toBe('2024-01-15');
expect(spy).toHaveBeenCalled();
expect(spy).toHaveBeenCalledTimes(1);
expect(spy).toHaveBeenCalledWith(new Date());
spy.mockRestore(); // restore original
});// ── Mocking Modules ──
// jest.mock() — auto-mock entire module
jest.mock('axios');
import axios from 'axios';
test('mocked axios', async () => {
axios.get.mockResolvedValue({ data: { id: 1, name: 'Alice' } });
const user = await fetchUser(1);
expect(user.name).toBe('Alice');
expect(axios.get).toHaveBeenCalledWith('/api/users/1');
});
// jest.mock() with factory function
jest.mock('../services/email', () => ({
sendEmail: jest.fn().mockResolvedValue(true),
sendBatch: jest.fn().mockResolvedValue({ sent: 5 }),
}));
// Mock only part of a module
jest.mock('lodash', () => {
const actual = jest.requireActual('lodash');
return {
...actual,
debounce: jest.fn((fn) => fn), // just pass through
};
});
// jest.doMock() — mock inside test (not hoisted)
test('dynamic mock', () => {
jest.doMock('../config', () => ({ API_URL: 'http://test-api' }));
const config = require('../config');
expect(config.API_URL).toBe('http://test-api');
jest.dontMock('../config');
});
// jest.spyOn with mockImplementation
jest.spyOn(fs, 'readFileSync')
.mockImplementation(() => '{"key": "value"}');// ── Timer Mocking ──
// Fake timers
jest.useFakeTimers();
afterEach(() => {
jest.useRealTimers(); // restore between tests
});
test('timer mocking', () => {
const callback = jest.fn();
setTimeout(callback, 1000);
// Fast-forward time
jest.advanceTimersByTime(1000);
expect(callback).toHaveBeenCalledTimes(1);
// Or run all pending timers
jest.runAllTimers();
// Or run timers to next tick
jest.runOnlyPendingTimers();
});
test('interval mocking', () => {
const callback = jest.fn();
setInterval(callback, 100);
jest.advanceTimersByTime(500);
expect(callback).toHaveBeenCalledTimes(5);
jest.clearAllTimers(); // clear all timers
});
// ── Mock Return Values ──
test('mock return patterns', () => {
const mock = jest.fn();
mock.mockReturnValue(42); // always returns 42
mock.mockReturnValueOnce(1) // 1st call → 1
.mockReturnValueOnce(2) // 2nd call → 2
.mockReturnValue(0); // 3rd+ calls → 0
expect(mock()).toBe(1);
expect(mock()).toBe(2);
expect(mock()).toBe(0);
// Async returns
mock.mockResolvedValue({ id: 1 }); // Promise.resolve
mock.mockResolvedValueOnce({ id: 2 }); // first call only
mock.mockRejectedValue(new Error('fail')); // Promise.reject
});| Property | Description |
|---|---|
| mock.calls | Array of all call arguments |
| mock.results | Array of all return values |
| mock.instances | "this" values for each call |
| mockClear() | Clear calls/results only |
| mockReset() | Clear all + remove implementation |
| mockRestore() | Reset + restore original (spy) |
| Method | Description |
|---|---|
| jest.mock("module") | Auto-mock entire module |
| jest.mock("m", factory) | Custom mock factory |
| jest.fn(impl) | Create mock with implementation |
| jest.spyOn(obj, "fn") | Spy on existing method |
| jest.requireActual("m") | Get real module in mock |
| jest.doMock("m") | Mock inside test (no hoist) |
| jest.replaceProperty() | Replace object property |
jest.doMock() inside the test function.// ── Async Test Patterns ──
// 1. Promise return (NO await)
test('fetches user data', () => {
return fetchUser(1).then(data => {
expect(data.name).toBe('Alice');
});
});
// 2. async/await (recommended)
test('fetches user data with async', async () => {
const data = await fetchUser(1);
expect(data.name).toBe('Alice');
});
// 3. .resolves / .rejects matchers
test('resolves to value', async () => {
await expect(fetchUser(1)).resolves.toMatchObject({
name: 'Alice',
});
});
test('rejects with error', async () => {
await expect(fetchUser(999)).rejects.toThrow('User not found');
});
// 4. Callback-based (done pattern)
test('callback completes', (done) => {
function callback(error, data) {
if (error) {
done(error);
return;
}
try {
expect(data).toBeDefined();
done();
} catch (err) {
done(err);
}
}
fetchData(callback);
});
// ════════════════════════════════════
// IMPORTANT: always return / await promises!
// ════════════════════════════════════
// ❌ WRONG — test finishes before promise resolves
test('bad async test', () => {
fetchUser(1).then(data => {
expect(data.name).toBe('Alice'); // never checked!
});
});
// ✅ CORRECT — returns the promise
test('good async test', () => {
return fetchUser(1).then(data => {
expect(data.name).toBe('Alice');
});
});// ── Testing API Calls with Mocks ──
import axios from 'axios';
import UserService from './UserService';
jest.mock('axios');
describe('UserService', () => {
beforeEach(() => {
jest.clearAllMocks();
});
test('getAll returns users', async () => {
const mockUsers = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
];
axios.get.mockResolvedValue({ data: mockUsers });
const users = await UserService.getAll();
expect(users).toEqual(mockUsers);
expect(axios.get).toHaveBeenCalledWith('/api/users');
});
test('create returns new user', async () => {
const newUser = { name: 'Charlie' };
axios.post.mockResolvedValue({ data: { id: 3, ...newUser } });
const result = await UserService.create(newUser);
expect(result.id).toBe(3);
expect(axios.post).toHaveBeenCalledWith('/api/users', newUser);
});
test('delete handles 404', async () => {
axios.delete.mockRejectedValue({ response: { status: 404 } });
await expect(UserService.delete(999)).rejects.toEqual(
expect.objectContaining({ response: { status: 404 } })
);
});
});
// ── waitFor & waitForElementToBeRemoved ──
import { render, screen, waitFor, waitForElementToBeRemoved } from '@testing-library/react';
test('loading state resolves', async () => {
render(<UserProfile userId={1} />);
expect(screen.getByText('Loading...')).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText('Alice')).toBeInTheDocument();
});
// Or wait for loading to disappear
await waitForElementToBeRemoved(() =>
screen.getByText('Loading...')
);
});| Pattern | Syntax | Best For |
|---|---|---|
| Return promise | return fn().then() | Simple promises |
| async/await | await fn() | Recommended |
| .resolves | await expect(p).resolves.toBe() | Clean one-liners |
| .rejects | await expect(p).rejects.toThrow() | Error testing |
| done callback | test("x", done => ...) | Callback APIs |
| waitFor | await waitFor(() => expect()) | React state changes |
| Option | Default | Description |
|---|---|---|
| timeout | 1000ms | Max wait time |
| interval | 50ms | Polling interval |
| onTimeout | throws | Custom timeout error |
| container | document | Scope to container |
// ── Snapshot Testing ──
// 1. Component snapshot
import renderer from 'react-test-renderer';
import MyComponent from './MyComponent';
test('matches snapshot', () => {
const tree = renderer.create(<MyComponent name="Alice" />).toJSON();
expect(tree).toMatchSnapshot();
});
// 2. Inline snapshot
test('inline snapshot', () => {
const output = formatUser({ id: 1, name: 'Alice', role: 'admin' });
expect(output).toMatchInlineSnapshot(`
"User #1: Alice (admin)"
`);
});
// 3. Property matchers in snapshots
test('snapshot with matchers', () => {
const user = {
id: expect.any(Number),
name: 'Alice',
createdAt: expect.any(Date),
token: expect.any(String),
};
expect(user).toMatchSnapshot();
});
// 4. Snapshot with custom name
test('user profile snapshot', () => {
expect(getProfile()).toMatchSnapshot('user-profile-v1');
});
// ── Update snapshots ──
// CLI: npx jest --updateSnapshot
// CLI: npx jest -u// ── Snapshot with Testing Library ──
import { render } from '@testing-library/react';
import UserCard from './UserCard';
test('UserCard matches snapshot', () => {
const { container } = render(
<UserCard name="Alice" role="Admin" avatar="/alice.jpg" />
);
expect(container).toMatchSnapshot();
});
// ── Multiple snapshots per test
describe('Button variants', () => {
test.each([
['primary', 'bg-blue-500'],
['secondary', 'bg-gray-200'],
['danger', 'bg-red-500'],
])('Button %s matches snapshot', (variant, expectedClass) => {
const { container } = render(
<Button variant={variant}>Click me</Button>
);
expect(container).toMatchSnapshot(`button-${variant}`);
expect(container.firstChild).toHaveClass(expectedClass);
});
});| Command | Description |
|---|---|
| jest --updateSnapshot | Update all failing snapshots |
| jest -u | Same as above (short) |
| jest --watch | Interactive update mode (press u) |
| jest --no-cache | Bypass snapshot cache |
| jest --testNamePattern | Update specific snapshots |
| Practice | Why |
|---|---|
| Keep snapshots small | Easier to review diffs |
| Use inline snapshots | Self-documenting, in test file |
| Use matchers for IDs/dates | Avoid flaky snapshots |
| Review diffs carefully | Ensure changes are intentional |
| Version snapshot names | Easier migration tracking |
| CI should fail on new snapshots | Prevent silent updates |
toMatchInlineSnapshot() for smaller snapshots — the expected value is written directly in the test file, making it self-documenting and easier to review in code reviews.expect.any() matchers for dynamic values like IDs, timestamps, and random tokens to prevent flaky snapshots that change on every run.// ── @testing-library/react ──
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Counter from './Counter';
// Recommended: render + screen pattern
describe('Counter', () => {
test('renders initial count', () => {
render(<Counter initialCount={5} />);
expect(screen.getByText('Count: 5')).toBeInTheDocument();
});
test('increments on click', async () => {
const user = userEvent.setup();
render(<Counter initialCount={0} />);
await user.click(screen.getByRole('button', { name: /increment/i }));
expect(screen.getByText('Count: 1')).toBeInTheDocument();
});
test('decrements on click', async () => {
const user = userEvent.setup();
render(<Counter initialCount={5} />);
await user.click(screen.getByRole('button', { name: /decrement/i }));
expect(screen.getByText('Count: 4')).toBeInTheDocument();
});
});// ── Form Testing ──
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import LoginForm from './LoginForm';
describe('LoginForm', () => {
test('submits form with valid data', async () => {
const handleSubmit = jest.fn();
const user = userEvent.setup();
render(<LoginForm onSubmit={handleSubmit} />);
await user.type(screen.getByLabelText(/email/i), 'alice@example.com');
await user.type(screen.getByLabelText(/password/i), 'password123');
await user.click(screen.getByRole('button', { name: /sign in/i }));
await waitFor(() => {
expect(handleSubmit).toHaveBeenCalledWith({
email: 'alice@example.com',
password: 'password123',
});
});
});
test('shows error for invalid email', async () => {
const user = userEvent.setup();
render(<LoginForm onSubmit={jest.fn()} />);
await user.type(screen.getByLabelText(/email/i), 'not-an-email');
await user.type(screen.getByLabelText(/password/i), 'pass');
await user.click(screen.getByRole('button', { name: /sign in/i }));
expect(screen.getByText(/invalid email/i)).toBeInTheDocument();
expect(screen.getByRole('button', { name: /sign in/i })).toBeDisabled();
});
});
// ── Query Priority (use in order) ──
// 1. getByRole → button, link, textbox, heading
// 2. getByLabelText → form inputs with labels
// 3. getByPlaceholderText → inputs with placeholder
// 4. getByText → text content
// 5. getByDisplayValue → form input current value
// 6. getByTestId → fallback (data-testid="...")// ── Mocking Hooks & Context ──
import { render, screen } from '@testing-library/react';
import { AuthProvider } from '../context/AuthContext';
import Dashboard from './Dashboard';
// Custom render with providers
function renderWithProviders(ui, { providerProps = {} } = {}) {
return render(
<AuthProvider {...providerProps}>{ui}</AuthProvider>
);
}
test('shows dashboard for authenticated user', () => {
renderWithProviders(<Dashboard />, {
providerProps: { isAuthenticated: true, user: { name: 'Alice' } },
});
expect(screen.getByText('Welcome, Alice')).toBeInTheDocument();
});
// Mock custom hooks
jest.mock('../hooks/useUser', () => ({
useUser: () => ({
user: { id: 1, name: 'Alice', role: 'admin' },
isLoading: false,
error: null,
}),
}));
// Mock fetch in component
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ data: [1, 2, 3] }),
})
);
beforeEach(() => {
jest.clearAllMocks();
});| Method | Throws if not found? | Use For |
|---|---|---|
| getBy... | Yes | Expected to exist |
| queryBy... | No (returns null) | May not exist |
| findBy... | Yes (async) | Appears after await |
| getAllBy... | Yes | Multiple matches |
| queryAllBy... | No | Multiple, may not exist |
| findAllBy... | Yes (async) | Multiple, async |
| Method | Description |
|---|---|
| click(element) | Click an element |
| dblClick(element) | Double click |
| type(element, text) | Type into input |
| selectOptions(el, vals) | Select dropdown options |
| clear(element) | Clear input field |
| tab() | Press Tab key |
| keyboard(keys) | Type keyboard shortcuts |
| upload(element, file) | Upload a file |
| hover(element) | Hover over element |
getByRole("button", { name: /submit/i }) is better than getByTestId("submit-btn").// ── jest.config.js ──
/** @type {import('jest').Config} */
module.exports = {
// Test environment
testEnvironment: 'jsdom', // 'node' | 'jsdom' (React)
// File patterns
testMatch: [
'**/__tests__/**/*.(test|spec).(js|jsx|ts|tsx)',
'**/*.(test|spec).(js|jsx|ts|tsx)',
],
testPathIgnorePatterns: ['/node_modules/', '/dist/', '/e2e/'],
modulePathIgnorePatterns: ['<rootDir>/build/'],
// Module resolution
moduleDirectories: ['node_modules', '<rootDir>/src'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1', // path aliases
'\.(css|less|scss)$': 'identity-obj-proxy', // mock CSS
'\.(jpg|jpeg|png|gif|webp|svg)$': '<rootDir>/__mocks__/fileMock.js',
},
// Setup files (run before each test suite)
setupFilesAfterSetup: ['<rootDir>/jest.setup.js'],
// Coverage
collectCoverageFrom: [
'src/**/*.{js,jsx,ts,tsx}',
'!src/**/*.d.ts',
'!src/**/*.stories.{js,jsx,ts,tsx}',
'!src/**/__tests__/**',
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
// Transforms
transform: {
'^.+\\.(ts|tsx)$': 'ts-jest',
},
// Reset mocks between tests
resetMocks: true,
restoreMocks: true,
// Parallelization
maxWorkers: '50%',
};// ── jest.setup.js — Global Test Setup ──
// Extend expect with jest-dom matchers
import '@testing-library/jest-dom';
// Mock window.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(),
})),
});
// Mock IntersectionObserver
class MockIntersectionObserver {
observe() {}
disconnect() {}
unobserve() {}
}
Object.defineProperty(window, 'IntersectionObserver', {
writable: true,
value: MockIntersectionObserver,
});
// Mock ResizeObserver
global.ResizeObserver = class {
observe() {}
disconnect() {}
unobserve() {}
};
// Suppress console.error in tests (optional)
// const originalError = console.error;
// beforeAll(() => {
// console.error = (...args) => {
// if (args[0].includes('Warning:')) return;
// originalError.call(console, ...args);
// };
// });
// afterAll(() => { console.error = originalError; });| Option | Description |
|---|---|
| testEnvironment | node, jsdom, jsdom-global |
| setupFilesAfterSetup | Setup before each suite |
| moduleNameMapper | Path aliases & asset mocks |
| testMatch | Glob patterns for test files |
| testPathIgnorePatterns | Exclude test directories |
| transform | Custom transforms (ts-jest, babel) |
| collectCoverageFrom | Files to measure coverage on |
| coverageThreshold | Min coverage percentages |
| maxWorkers | Parallel workers (50%, 1, 2) |
| clearMocks | Auto-clear mocks between tests |
| Environment | Use Case |
|---|---|
| node | Backend/CLI tests (default) |
| jsdom | DOM testing (React, Vue) |
| jsdom-global | DOM + global scope |
| happy-dom | Fast jsdom alternative |
| @edge-runtime/jest | Edge runtime tests |
moduleNameMapper to handle path aliases (e.g., @/) and mock static assets (CSS, images). Without it, tests will fail on import './styles.module.css'.