jest — Cursor rule: how to write Jest tests
A Cursor rule that fixes one style for Jest tests: config, isolation, mocks and async checks. Useful when several people write tests in the same repo.
.cursor/rules/jest.mdcWhat jest does
The file .cursor/rules/jest.mdc plugs into Cursor and tells the agent how to write tests in JavaScript and TypeScript projects. Scope comes from globs: **/*.{js,ts,jsx,tsx}.
Inside is a set of conventions. Keep config in jest.config.ts with the ts-jest preset, clearMocks and resetMocks on, coverage collected from src/**/*.{js,jsx,ts,tsx} with declarations and entry points excluded. Put tests next to the code — in __tests__/ or a *.test.ts file — not in a separate tests/ folder. Assert on public behaviour instead of private fields. Use async/await rather than .then() chains. Isolation comes from beforeEach and afterEach, dependencies get replaced with jest.mock and jest.spyOn, time with jest.useFakeTimers.
The rule assumes your team already agreed on this style. If you run a different preset or keep tests in a separate folder, expect to rewrite part of the advice.
What it helps with
- settle on one Jest config across the team
- keep tests next to the code instead of a separate folder
- assert on public behaviour rather than private fields
- replace external dependencies with
jest.mockandjest.spyOn - isolate tests with
beforeEachandafterEach
Example prompts for the agent
- write tests for
src/utils/formatDate.tsfollowing the jest rule - rewrite this test so it stops touching private class fields
- add a mock for the
./apimodule in the service test
How to install jest
Manually: put the files in place
.cursor/rules/jest.mdcFrontmatter: description, globs, alwaysApply
Manually: put the files in place
.claude/rules/jest.mdThe paths frontmatter field limits the files: move globs from the source into paths
AGENTS.md
Codex reads AGENTS.md: paste the rule text there
Manually: put the files in place
.github/instructions/jest.instructions.mdThe applyTo field sets which files it applies to: move globs from the source into applyTo
Manually: put the files in place
.devin/rules/jest.mdManually: put the files in place
.clinerules/jest.mdStep by step, with commands for each platform: Cursor rules: .mdc files, four modes and what replaced .cursorrules
Questions about jest
How do I install the jest rule in Cursor?
Create .cursor/rules/jest.mdc in your project root and paste the rule contents into it. Cursor picks it up automatically for files matching globs.
What does the jest rule do?
It sets conventions for Jest tests: config in jest.config.ts, tests colocated with code, isolation through beforeEach and afterEach, and handling of mocks and async code.
Does this rule require ts-jest?
The sample config uses the ts-jest preset for TypeScript files. If your project runs a different preset, adapt the config to match.
Source text
jest.mdc · CC0-1.0
---
description: Definitive guidelines for writing robust, maintainable, and performant Jest tests in JavaScript and TypeScript projects.
globs: **/*.{js,ts,jsx,tsx}
---
# 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.
```typescript
// jest.config.ts
import type { Config } from 'jest';
import { defaults } from 'jest-config';
const config: Config = {
// Use ts-jest for TypeScript files
preset: 'ts-jest',
// Automatically clear mock calls and instances between tests
clearMocks: true,
// Reset mocks between tests to prevent state leakage
resetMocks: true,
// Detailed test output
verbose: true,
// Collect coverage from specific files, exclude common boilerplate
collectCoverageFrom: [
'src/**/*.{js,jsx,ts,tsx}',
'!src/**/*.d.ts',
'!src/index.tsx', // Exclude entry points
'!src/reportWebVitals.ts',
],
// Ignore coverage for specific paths
coveragePathIgnorePatterns: [
'/node_modules/',
'src/setupTests.ts',
],
// Setup file for global test environment configurations (e.g., @testing-library/jest-dom)
setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'],
// Map module paths for aliases or specific packages
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1', // Example: for absolute imports
},
// Test environment for browser-like DOM APIs
testEnvironment: 'jsdom',
// Match test files ending with .test.ts(x) or .spec.ts(x)
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
```typescript
class UserService {
private _users: User[] = []; // Internal state
constructor() { /* ... */ }
async fetchUsers() { /* ... */ }
}
test('UserService should have empty _users array initially', () => {
const service = new UserService();
expect(service._users).toHaveLength(0); // Accessing private property
});
```
✅ GOOD: Testing public behavior
```typescript
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); // Testing public method output
});
```
### Descriptive Test Names
Use clear, concise names that explain *what* is being tested and *what* the expected outcome is.
❌ BAD: Vague names
```typescript
test('adds', () => { /* ... */ });
it('should work', () => { /* ... */ });
```
✅ GOOD: Specific and expressive names
```typescript
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.
```typescript
let mockData: any[];
beforeEach(() => {
mockData = [{ id: 1, name: 'Alice' }];
});
afterEach(() => {
// Clean up any global mocks or side effects if not handled by config.resetMocks
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); // mockData is reset by beforeEach
});
```
## 4. Asynchronous Testing
Always use `async/await` for asynchronous code.
❌ BAD: Using `.then()` or forgetting `await`
```typescript
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); // Test finishes before promise resolves
expect(user).toBeDefined(); // This will pass, but user is a Promise
});
```
✅ GOOD: Using `async/await`
```typescript
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.
```typescript
// api.ts
export const fetchData = async () => { /* ... */ };
// service.ts
import { fetchData } from './api';
export const getServiceData = async () => fetchData();
// service.test.ts
import { getServiceData } from './service';
import { fetchData } from './api';
jest.mock('./api'); // Mocks the entire module
test('getServiceData should call fetchData', async () => {
(fetchData as jest.Mock).mockResolvedValue('mocked data'); // Cast to jest.Mock
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).
```typescript
// calculator.ts
export const add = (a: number, b: number) => a + b;
export const calculate = (a: number, b: number) => add(a, b) * 2;
// calculator.test.ts
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(); // Clean up the spy
});
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).toBe(200); // 100 * 2
addSpy.mockRestore();
});
```
### Fake Timers (`jest.useFakeTimers`)
Control `setTimeout`, `setInterval`, `Date` for predictable tests involving time.
```typescript
// timer.ts
export const runAfterDelay = (cb: () => void) => setTimeout(cb, 1000);
// timer.test.ts
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); // Advance by 500ms
expect(callback).not.toHaveBeenCalled();
jest.advanceTimersByTime(500); // Advance by another 500ms (total 1000ms)
expect(callback).toHaveBeenCalledTimes(1);
});
jest.useRealTimers(); // Restore real timers after tests
```
### 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.
```typescript
// src/setupTests.ts (referenced in jest.config.ts)
import '@testing-library/jest-dom';
// src/components/Button/index.tsx
type ButtonProps = { onClick: () => void; children: React.ReactNode; };
export const Button = ({ onClick, children }: ButtonProps) => (
<button onClick={onClick}>{children}</button>
);
// src/components/Button/__tests__/Button.test.tsx
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', { name: /click me/i });
expect(button).toBeInTheDocument();
await user.click(button);
expect(handleClick).toHaveBeenCalledTimes(1);
});
```
### Avoid `screen.debug()` in Committed Code
`screen.debug()` is for debugging during development, not for production tests.
❌ BAD:
```typescript
test('renders component', () => {
render(<MyComponent />);
screen.debug(); // Should not be committed
expect(screen.getByText('Hello')).toBeInTheDocument();
});
```
✅ GOOD:
```typescript
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.
```typescript
// src/components/Card/__tests__/Card.test.tsx
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();
});
// For GraphQL schema stability (requires jest-serializer-graphql-schema)
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:
```typescript
describe.only('My Feature', () => { /* ... */ });
test.skip('should not run this test', () => { /* ... */ });
```
✅ GOOD:
```typescript
describe('My Feature', () => { /* ... */ });
test('should run this test', () => { /* ... */ });
```
© sanjeed5 · CC0-1.0 · sanjeed5/awesome-cursor-rules-mdc