| name | jest |
| description | [Applies to: **/*.{js,ts,jsx,tsx}] Definitive guidelines for writing robust, maintainable, and performant Jest tests in JavaScript and TypeScript projects. |
| source | cursor_mdc |
jest Best Practices
This guide outlines our team's definitive Jest best practices. Adhere to these rules for consistent, reliable, and efficient testing.
1. Configuration (jest.config.ts)
Always use a jest.config.ts file for type safety and explicit configuration.
import type { Config } from 'jest';
import { defaults } from 'jest-config';
const config: Config = {
preset: 'ts-jest',
clearMocks: true,
resetMocks: true,
verbose: true,
collectCoverageFrom: [
'src/**/*.{js,jsx,ts,tsx}',
'!src/**/*.d.ts',
'!src/index.tsx',
'!src/reportWebVitals.ts',
],
coveragePathIgnorePatterns: [
'/node_modules/',
'src/setupTests.ts',
],
setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
},
testEnvironment: 'jsdom',
testMatch: [...defaults.testMatch, '**/?(*.)+(spec|test).[tj]s?(x)'],
};
export default config;
2. Test Organization
Colocate test files with the code they test. This improves discoverability and maintainability.
❌ BAD: Separate tests/ directory
src/
components/
Button.tsx
tests/
components/
Button.test.tsx
✅ GOOD: Colocated __tests__/ or .test.ts
src/
components/
Button/
index.tsx
__tests__/
Button.test.tsx
utils/
formatDate.ts
formatDate.test.ts
3. Core Principles
Test Behavior, Not Implementation
Focus on the public API and expected outcomes. Refactoring internal logic should not break tests.
❌ BAD: Testing internal state or private methods
class UserService {
private _users: User[] = [];
constructor() { }
async fetchUsers() { }
}
test('UserService should have empty _users array initially', () => {
const service = new UserService();
expect(service._users).toHaveLength(0);
});
✅ GOOD: Testing public behavior
class UserService {
private _users: User[] = [];
constructor() { }
async fetchUsers() { return this._users; }
}
test('UserService should return an empty array of users initially', async () => {
const service = new UserService();
const users = await service.fetchUsers();
expect(users).toHaveLength(0);
});
Descriptive Test Names
Use clear, concise names that explain what is being tested and what the expected outcome is.
❌ BAD: Vague names
test('adds', () => { });
it('should work', () => { });
✅ GOOD: Specific and expressive names
describe('sum function', () => {
test('should correctly add two positive numbers', () => { });
test('should return zero when adding zero to a number', () => { });
});
Keep Tests Isolated
Each test should run independently without relying on the state or side effects of other tests. Use beforeEach and afterEach for setup/teardown.
let mockData: any[];
beforeEach(() => {
mockData = [{ id: 1, name: 'Alice' }];
});
afterEach(() => {
jest.clearAllMocks();
});
test('should add a new item to mockData', () => {
mockData.push({ id: 2, name: 'Bob' });
expect(mockData).toHaveLength(2);
});
test('should retrieve the initial mockData', () => {
expect(mockData).toHaveLength(1);
});
4. Asynchronous Testing
Always use async/await for asynchronous code.
❌ BAD: Using .then() or forgetting await
test('should fetch user data', () => {
fetchUser(1).then(user => {
expect(user.id).toBe(1);
});
});
test('should not fetch user data (missing await)', () => {
const user = fetchUser(1);
expect(user).toBeDefined();
});
✅ GOOD: Using async/await
test('should fetch user data correctly', async () => {
const user = await fetchUser(1);
expect(user.id).toBe(1);
});
test('should handle fetch error', async () => {
await expect(fetchUser(999)).rejects.toThrow('User not found');
});
5. Mocking Strategies
Mock external dependencies to isolate the unit under test and ensure deterministic, fast tests.
Automatic Mocks (jest.mock)
Use jest.mock for entire modules.
export const fetchData = async () => { };
import { fetchData } from './api';
export const getServiceData = async () => fetchData();
import { getServiceData } from './service';
import { fetchData } from './api';
jest.mock('./api');
test('getServiceData should call fetchData', async () => {
(fetchData as jest.Mock).mockResolvedValue('mocked data');
const data = await getServiceData();
expect(data).toBe('mocked data');
expect(fetchData).toHaveBeenCalledTimes(1);
});
Spying on Functions (jest.spyOn)
Use jest.spyOn to observe calls to existing functions without replacing their original implementation (unless you explicitly mock it).
export const add = (a: number, b: number) => a + b;
export const calculate = (a: number, b: number) => add(a, b) * 2;
import * as calculator from './calculator';
test('calculate should call add', () => {
const addSpy = jest.spyOn(calculator, 'add');
calculator.calculate(1, 2);
expect(addSpy).toHaveBeenCalledWith(1, 2);
addSpy.mockRestore();
});
test('calculate should return mocked value if add is mocked', () => {
const addSpy = jest.spyOn(calculator, 'add').mockReturnValue(100);
const result = calculator.calculate(1, 2);
expect(result).();
addSpy.();
});
Fake Timers (jest.useFakeTimers)
Control setTimeout, setInterval, Date for predictable tests involving time.
export const runAfterDelay = (cb: () => void) => setTimeout(cb, 1000);
import { runAfterDelay } from './timer';
jest.useFakeTimers();
test('runAfterDelay calls the callback after 1 second', () => {
const callback = jest.fn();
runAfterDelay(callback);
expect(callback).not.toHaveBeenCalled();
jest.advanceTimersByTime(500);
expect(callback).not.toHaveBeenCalled();
jest.advanceTimersByTime(500);
expect(callback).toHaveBeenCalledTimes(1);
});
jest.useRealTimers();
Avoid Over-Mocking
Mock only what's necessary. Over-mocking can lead to brittle tests that don't reflect real-world behavior.
6. UI Component Testing (with React Testing Library)
For React components, use React Testing Library with @testing-library/jest-dom matchers.
import '@testing-library/jest-dom';
type ButtonProps = { onClick: () => void; children: React.ReactNode; };
export const Button = ({ onClick, children }: ButtonProps) => (
<button onClick={onClick}>{children}</button>
);
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Button } from '../';
test('Button renders children and handles click', async () => {
const user = userEvent.setup();
const handleClick = jest.fn();
render(<Button onClick={handleClick}>Click Me</Button>);
const button = screen.getByRole(, { : });
(button).();
user.(button);
(handleClick).();
});
Avoid screen.debug() in Committed Code
screen.debug() is for debugging during development, not for production tests.
❌ BAD:
test('renders component', () => {
render(<MyComponent />);
screen.debug();
expect(screen.getByText('Hello')).toBeInTheDocument();
});
✅ GOOD:
test('renders component', () => {
render(<MyComponent />);
expect(screen.getByText('Hello')).toBeInTheDocument();
});
7. Snapshot Testing
Use snapshot tests for UI components or schema stability, but review them carefully.
import { render } from '@testing-library/react';
import { Card } from '../';
test('Card component matches snapshot', () => {
const { asFragment } = render(<Card title="Test" content="Hello World" />);
expect(asFragment()).toMatchSnapshot();
});
import { buildSchema } from 'graphql';
import { lexicographicSortSchema } from 'graphql/utilities';
test('GraphQL schema is stable', () => {
const schema = buildSchema();
expect(lexicographicSortSchema(schema)).toMatchSnapshot();
});
8. Avoid Focused Tests in Committed Code
Never commit .only or .skip to the codebase. These are for local development only.
❌ BAD:
describe.only('My Feature', () => { });
test.skip('should not run this test', () => { });
✅ GOOD:
describe('My Feature', () => { });
test('should run this test', () => { });