Skip to main content
rule

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.

Install into Cursor · Manually: put the files in place All methods and platforms ↓
.cursor/rules/jest.mdc
Open on GitHub

What 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.mock and jest.spyOn
  • isolate tests with beforeEach and afterEach

Example prompts for the agent

  • write tests for src/utils/formatDate.ts following the jest rule
  • rewrite this test so it stops touching private class fields
  • add a mock for the ./api module in the service test

How to install jest

Manually: put the files in place

.cursor/rules/jest.mdc

Frontmatter: description, globs, alwaysApply

Step 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

Similar IDE rules

Everything for Testing and QA →

From the same set69

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor