# SkillPatch skill: jest-skill

A comprehensive Jest testing skill for JavaScript and TypeScript that guides agents in writing unit and integration tests. Covers core patterns including mocking, async testing, snapshot testing, and React component testing with Testing Library. Includes anti-patterns, quick-reference commands, and ready-to-use code examples.

You (the agent) were given this URL and asked to use this skill. This is a **public** skill — no SkillPatch account, API key, or prior setup is required. Two ways to use it:

**1. Use it right now** — the skill's complete file tree (SKILL.md + all reference files) is inlined below; read `SKILL.md` first, then follow it, consulting the other files as it directs.

**2. Install the exact package onto disk** (recommended if you can run a shell — this reproduces the skill byte-for-byte, including any binary assets that can't be inlined):

```bash
mkdir -p .claude/skills/jest-skill
curl -sSL https://skillpatch.dev/install_skill/jest-skill | tar -xz -C .claude/skills/
```

(`.claude/skills/` is Claude Code's convention; use whatever directory your agent loads skills from.)


---

## Skill files (3)

- `SKILL.md`
- `reference/advanced-patterns.md`
- `reference/playbook.md`


### `SKILL.md`

````markdown
---
name: jest-skill
description: >
  Generates Jest unit and integration tests in JavaScript or TypeScript.
  Covers mocking, snapshots, async testing, and React component testing.
  Use when user mentions "Jest", "describe/it/expect", "jest.mock",
  "toMatchSnapshot". Triggers on: "Jest", "expect().toBe()", "jest.mock",
  "snapshot test", "JS test", "React test".
languages:
  - JavaScript
  - TypeScript
category: unit-testing
license: MIT
metadata:
  author: TestMu AI
  version: "1.0"
---

# Jest Testing Skill

## Core Patterns

### Basic Test

```javascript
describe('Calculator', () => {
  let calc;
  beforeEach(() => { calc = new Calculator(); });

  test('adds two numbers', () => {
    expect(calc.add(2, 3)).toBe(5);
  });

  test('throws on division by zero', () => {
    expect(() => calc.divide(10, 0)).toThrow('Division by zero');
  });
});
```

### Matchers

```javascript
expect(value).toBe(exact);                 // === strict
expect(value).toEqual(object);             // deep equality
expect(value).toBeTruthy();
expect(value).toBeNull();
expect(value).toBeGreaterThan(3);
expect(value).toBeCloseTo(0.3, 5);
expect(str).toMatch(/regex/);
expect(arr).toContain(item);
expect(arr).toHaveLength(3);
expect(obj).toHaveProperty('name');
expect(obj).toMatchObject({ name: 'Alice' });
expect(() => fn()).toThrow(CustomError);
```

### Mocking

```javascript
// Mock function
const mockFn = jest.fn();
mockFn.mockReturnValue(42);
mockFn.mockResolvedValue({ data: 'test' });
expect(mockFn).toHaveBeenCalledWith('arg1');
expect(mockFn).toHaveBeenCalledTimes(1);

// Mock module
jest.mock('./database');
const db = require('./database');
db.getUser.mockResolvedValue({ name: 'Alice' });

// Mock with implementation
jest.mock('./api', () => ({
  fetchUsers: jest.fn().mockResolvedValue([{ name: 'Alice' }]),
}));

// Spy
const spy = jest.spyOn(console, 'log').mockImplementation();
expect(spy).toHaveBeenCalledWith('expected');
spy.mockRestore();

// Fake timers
jest.useFakeTimers();
jest.advanceTimersByTime(1000);
jest.useRealTimers();
```

### Async Testing

```javascript
test('fetches users', async () => {
  const users = await fetchUsers();
  expect(users).toHaveLength(3);
});

test('resolves with data', () => {
  return expect(fetchData()).resolves.toEqual({ data: 'value' });
});

test('rejects with error', () => {
  return expect(fetchBadData()).rejects.toThrow('not found');
});
```

### React Component Testing (Testing Library)

```javascript
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import '@testing-library/jest-dom';
import LoginForm from './LoginForm';

test('submits login form', async () => {
  const onSubmit = jest.fn();
  render(<LoginForm onSubmit={onSubmit} />);

  fireEvent.change(screen.getByLabelText('Email'), {
    target: { value: 'user@test.com' },
  });
  fireEvent.change(screen.getByLabelText('Password'), {
    target: { value: 'password123' },
  });
  fireEvent.click(screen.getByRole('button', { name: /login/i }));

  await waitFor(() => {
    expect(onSubmit).toHaveBeenCalledWith({
      email: 'user@test.com', password: 'password123',
    });
  });
});
```

### Snapshot Testing

```javascript
test('renders correctly', () => {
  const tree = renderer.create(<Button label="Click" />).toJSON();
  expect(tree).toMatchSnapshot();
});
// Update: jest --updateSnapshot
```

### Anti-Patterns

| Bad | Good | Why |
|-----|------|-----|
| `expect(x === y).toBe(true)` | `expect(x).toBe(y)` | Better errors |
| No `await` on async | Always `await` | Swallows failures |
| Snapshot everything | Snapshot UI, assert logic | Snapshot fatigue |

## Quick Reference

| Task | Command |
|------|---------|
| Run all | `npx jest` |
| Watch | `npx jest --watch` |
| Coverage | `npx jest --coverage` |
| Update snapshots | `npx jest --updateSnapshot` |
| Run file | `npx jest tests/calc.test.js` |
| Single test | `test.only('name', () => {})` |

## Deep Patterns

For production-grade patterns, see `reference/playbook.md`:

| Section | What's Inside |
|---------|--------------|
| §1 Production Config | Node + React configs, path aliases, coverage thresholds |
| §2 Mocking Deep Dive | Module/partial/manual mocks, spies, timers, env vars |
| §3 Async Patterns | Promises, rejections, event emitters, streams |
| §4 test.each | Array, tagged template, describe.each for table-driven tests |
| §5 Custom Matchers | toBeWithinRange, toBeValidEmail, TypeScript declarations |
| §6 React Testing Library | userEvent, hooks, context providers |
| §7 Snapshot Testing | Component, inline, property matchers |
| §8 API Service Testing | Mocked axios, CRUD patterns, error handling |
| §9 Global Setup | Multi-project config, DB setup/teardown |
| §10 CI/CD | GitHub Actions with coverage gates |
| §11 Debugging Table | 10 common problems with fixes |
| §12 Best Practices | 15-item production checklist |

````


### `reference/advanced-patterns.md`

````markdown
# Jest — Advanced Patterns & Playbook

## Custom Matchers

```javascript
expect.extend({
  toBeWithinRange(received, floor, ceiling) {
    const pass = received >= floor && received <= ceiling;
    return {
      pass,
      message: () => `expected ${received} to be within range ${floor} - ${ceiling}`
    };
  },
  toContainObject(received, argument) {
    const pass = this.equals(received, expect.arrayContaining([expect.objectContaining(argument)]));
    return { pass, message: () => `expected array to contain object ${JSON.stringify(argument)}` };
  }
});

test('value in range', () => expect(100).toBeWithinRange(90, 110));
```

## Advanced Mocking Patterns

```javascript
// Module factory mock with partial implementation
jest.mock('./api', () => ({
  ...jest.requireActual('./api'),
  fetchUser: jest.fn(),
  deleteUser: jest.fn()
}));

// Mock class with auto-mocked methods
jest.mock('./services/UserService');
const { UserService } = require('./services/UserService');
UserService.prototype.getUser.mockResolvedValue({ id: 1, name: 'Alice' });

// Spy on module method while keeping implementation
const utils = require('./utils');
jest.spyOn(utils, 'formatDate').mockReturnValue('2025-01-01');

// Manual mock with __mocks__ directory
// __mocks__/axios.js
module.exports = {
  get: jest.fn(() => Promise.resolve({ data: {} })),
  post: jest.fn(() => Promise.resolve({ data: {} })),
  create: jest.fn(function () { return this; }),
  interceptors: { request: { use: jest.fn() }, response: { use: jest.fn() } }
};

// Timer mocking
jest.useFakeTimers();
test('debounced function', () => {
  const fn = jest.fn();
  const debounced = debounce(fn, 500);
  debounced(); debounced(); debounced();
  expect(fn).not.toHaveBeenCalled();
  jest.advanceTimersByTime(500);
  expect(fn).toHaveBeenCalledTimes(1);
});

// Mock date
jest.useFakeTimers({ now: new Date('2025-06-15T12:00:00Z') });
test('formats today', () => expect(getToday()).toBe('2025-06-15'));
```

## Async Testing Patterns

```javascript
// Async/await with error handling
test('rejects on network error', async () => {
  fetchUser.mockRejectedValueOnce(new Error('Network Error'));
  await expect(getUserProfile(1)).rejects.toThrow('Network Error');
});

// Testing streams/events
test('emits data events', (done) => {
  const stream = createReadStream('test.txt');
  const chunks = [];
  stream.on('data', chunk => chunks.push(chunk));
  stream.on('end', () => { expect(chunks.length).toBeGreaterThan(0); done(); });
});

// Retry logic testing
test('retries failed requests', async () => {
  fetchUser
    .mockRejectedValueOnce(new Error('timeout'))
    .mockRejectedValueOnce(new Error('timeout'))
    .mockResolvedValueOnce({ id: 1 });
  const result = await fetchWithRetry(fetchUser, 3);
  expect(result).toEqual({ id: 1 });
  expect(fetchUser).toHaveBeenCalledTimes(3);
});

// waitFor pattern with polling
const waitFor = (fn, { timeout = 5000, interval = 50 } = {}) =>
  new Promise((resolve, reject) => {
    const start = Date.now();
    const check = async () => {
      try { resolve(await fn()); }
      catch (e) {
        if (Date.now() - start >= timeout) reject(e);
        else setTimeout(check, interval);
      }
    };
    check();
  });
```

## Snapshot Testing

```javascript
// Inline snapshots (auto-filled by Jest)
test('user object shape', () => {
  expect(createUser('Alice')).toMatchInlineSnapshot(`
    { "id": Any<String>, "name": "Alice", "createdAt": Any<Date> }
  `);
});

// Property matchers for dynamic values
test('user with dynamic fields', () => {
  expect(createUser('Bob')).toMatchSnapshot({
    id: expect.any(String),
    createdAt: expect.any(Date)
  });
});

// Serializer for custom types
expect.addSnapshotSerializer({
  test: val => val && val.type === 'Component',
  serialize: (val) => `<${val.name} props={${JSON.stringify(val.props)}} />`
});
```

## React Testing with Testing Library

```javascript
import { render, screen, waitFor, within, act } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

// Complete component test
describe('TodoList', () => {
  const user = userEvent.setup();

  it('adds and removes todo items', async () => {
    render(<TodoList />);
    const input = screen.getByPlaceholderText('Add a todo');
    await user.type(input, 'Buy milk');
    await user.click(screen.getByRole('button', { name: /add/i }));
    expect(screen.getByText('Buy milk')).toBeInTheDocument();

    await user.click(screen.getByRole('button', { name: /delete/i }));
    expect(screen.queryByText('Buy milk')).not.toBeInTheDocument();
  });

  it('shows loading state', async () => {
    render(<TodoList />);
    expect(screen.getByRole('progressbar')).toBeInTheDocument();
    await waitFor(() => expect(screen.queryByRole('progressbar')).not.toBeInTheDocument());
  });

  it('handles form validation', async () => {
    render(<TodoList />);
    await user.click(screen.getByRole('button', { name: /add/i }));
    expect(screen.getByRole('alert')).toHaveTextContent('Todo cannot be empty');
  });
});

// Context/Provider testing
const renderWithProviders = (ui, { initialState, ...options } = {}) => {
  const store = createStore(initialState);
  const Wrapper = ({ children }) => (
    <ThemeProvider><StoreProvider store={store}>{children}</StoreProvider></ThemeProvider>
  );
  return { ...render(ui, { wrapper: Wrapper, ...options }), store };
};

// Hook testing
import { renderHook, act } from '@testing-library/react';
test('useCounter', () => {
  const { result } = renderHook(() => useCounter(0));
  act(() => result.current.increment());
  expect(result.current.count).toBe(1);
});
```

## Performance Testing with Jest

```javascript
test('processes 10k items under 100ms', () => {
  const items = Array.from({ length: 10000 }, (_, i) => ({ id: i }));
  const start = performance.now();
  processItems(items);
  expect(performance.now() - start).toBeLessThan(100);
});
```

## Configuration Best Practices

```javascript
// jest.config.js — production-grade
module.exports = {
  testEnvironment: 'jsdom',
  roots: ['<rootDir>/src'],
  collectCoverageFrom: ['src/**/*.{js,jsx,ts,tsx}', '!src/**/*.d.ts', '!src/index.{js,ts}'],
  coverageThresholds: { global: { branches: 80, functions: 80, lines: 80, statements: 80 } },
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1',
    '\\.(css|less|scss)$': 'identity-obj-proxy',
    '\\.(jpg|jpeg|png|svg)$': '<rootDir>/__mocks__/fileMock.js'
  },
  setupFilesAfterSetup: ['<rootDir>/jest.setup.js'],
  transform: { '^.+\\.(ts|tsx)$': 'ts-jest' },
  testMatch: ['**/__tests__/**/*.(spec|test).[jt]s?(x)'],
  watchPlugins: ['jest-watch-typeahead/filename', 'jest-watch-typeahead/testname']
};
```

## Anti-Patterns

- ❌ `test('works', () => { myFunction(); })` — assertion-free tests prove nothing
- ❌ Testing implementation details (internal state, private methods)
- ❌ Snapshot overuse — large snapshots become meaningless; prefer targeted assertions
- ❌ `jest.mock()` at file level when only one test needs it — use `jest.spyOn` per-test instead
- ❌ `setTimeout` in tests — use `jest.useFakeTimers()` and `advanceTimersByTime`
- ❌ Shared mutable state between tests — always reset in `beforeEach`

````


### `reference/playbook.md`

````markdown
# Jest — Advanced Implementation Playbook

## §1 — Production Config

```javascript
// jest.config.js
module.exports = {
  testEnvironment: 'node',
  roots: ['<rootDir>/src', '<rootDir>/tests'],
  testMatch: ['**/*.test.{js,ts}'],
  transform: { '^.+\\.tsx?$': 'ts-jest' },
  collectCoverage: true,
  coverageDirectory: 'coverage',
  coverageReporters: ['text', 'lcov', 'json-summary'],
  coverageThreshold: { global: { branches: 80, functions: 80, lines: 80, statements: 80 } },
  coveragePathIgnorePatterns: ['/node_modules/', '/tests/', '/dist/'],
  setupFilesAfterSetup: ['<rootDir>/tests/setup.ts'],
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1',
    '\\.(css|less|scss)$': 'identity-obj-proxy',
  },
  verbose: true,
  testTimeout: 10000,
  maxWorkers: '50%',  // Use half CPU cores
};
```

```javascript
// jest.config.js — for React projects
module.exports = {
  testEnvironment: 'jsdom',
  setupFilesAfterSetup: ['@testing-library/jest-dom', '<rootDir>/tests/setup.ts'],
  transform: { '^.+\\.(t|j)sx?$': ['@swc/jest'] },  // Faster than ts-jest
  moduleNameMapper: {
    '\\.(css|scss)$': 'identity-obj-proxy',
    '\\.(jpg|png|svg)$': '<rootDir>/tests/__mocks__/fileMock.js',
  },
};
```

## §2 — Mocking Deep Dive

```javascript
// Full module mock
jest.mock('./database', () => ({
  query: jest.fn(),
  connect: jest.fn().mockResolvedValue(true),
}));

// Partial mock (keep real implementations for some functions)
jest.mock('./utils', () => ({
  ...jest.requireActual('./utils'),
  fetchData: jest.fn(),
}));

// Manual mock — __mocks__/axios.js
module.exports = {
  get: jest.fn(() => Promise.resolve({ data: {} })),
  post: jest.fn(() => Promise.resolve({ data: {} })),
  create: jest.fn(function() { return this; }),
};

// Spy with chained responses
const spy = jest.spyOn(userService, 'findById')
  .mockResolvedValueOnce({ id: 1, name: 'Alice' })
  .mockResolvedValueOnce(null);

expect(await userService.findById(1)).toEqual({ id: 1, name: 'Alice' });
expect(await userService.findById(999)).toBeNull();
spy.mockRestore();

// Timer mocking
jest.useFakeTimers();
const callback = jest.fn();
setTimeout(callback, 5000);
jest.advanceTimersByTime(5000);
expect(callback).toHaveBeenCalledTimes(1);
jest.useRealTimers();

// Mock Date
jest.useFakeTimers({ now: new Date('2024-12-25T00:00:00Z') });
expect(new Date().toISOString()).toBe('2024-12-25T00:00:00.000Z');

// Mock environment variables
const originalEnv = process.env;
beforeEach(() => { process.env = { ...originalEnv, API_KEY: 'test-key' }; });
afterEach(() => { process.env = originalEnv; });

// Mock console
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
afterEach(() => consoleSpy.mockRestore());
```

## §3 — Async Patterns

```javascript
// Async/await
test('fetches user', async () => {
  const user = await fetchUser(1);
  expect(user.name).toBe('Alice');
});

// Rejection
test('rejects on not found', async () => {
  await expect(fetchUser(999)).rejects.toThrow('Not found');
});

// Resolves/rejects matchers
test('resolves with data', () => {
  return expect(fetchUser(1)).resolves.toMatchObject({ name: 'Alice' });
});

// Testing event emitters
test('emits data event', (done) => {
  const emitter = createDataStream();
  emitter.on('data', (data) => {
    expect(data).toBeDefined();
    done();
  });
  emitter.start();
});

// Testing streams / callbacks with timeout
test('stream completes', async () => {
  const result = await new Promise((resolve, reject) => {
    const chunks = [];
    stream.on('data', chunk => chunks.push(chunk));
    stream.on('end', () => resolve(Buffer.concat(chunks)));
    stream.on('error', reject);
  });
  expect(result.length).toBeGreaterThan(0);
});
```

## §4 — Table-Driven Tests (test.each)

```javascript
// Array format
test.each([
  ['admin@test.com', 'pass123', true],
  ['user@test.com', 'wrong', false],
  ['', '', false],
  ['invalid-email', 'pass', false],
])('login(%s, %s) => %s', async (email, password, expected) => {
  const result = await authService.login(email, password);
  expect(result.success).toBe(expected);
});

// Tagged template literal
test.each`
  input         | expected
  ${1}          | ${'1'}
  ${null}       | ${''}
  ${undefined}  | ${''}
  ${'hello'}    | ${'hello'}
`('toString($input) => $expected', ({ input, expected }) => {
  expect(toString(input)).toBe(expected);
});

// describe.each for grouped tests
describe.each([
  { role: 'admin', canDelete: true, canEdit: true },
  { role: 'editor', canDelete: false, canEdit: true },
  { role: 'viewer', canDelete: false, canEdit: false },
])('$role permissions', ({ role, canDelete, canEdit }) => {
  test(`canDelete is ${canDelete}`, () => {
    expect(getPermissions(role).canDelete).toBe(canDelete);
  });
  test(`canEdit is ${canEdit}`, () => {
    expect(getPermissions(role).canEdit).toBe(canEdit);
  });
});
```

## §5 — Custom Matchers

```javascript
// tests/setup.ts
expect.extend({
  toBeWithinRange(received, floor, ceiling) {
    const pass = received >= floor && received <= ceiling;
    return {
      message: () => `expected ${received} to be within [${floor}, ${ceiling}]`,
      pass,
    };
  },
  toBeValidEmail(received) {
    const pass = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(received);
    return {
      message: () => `expected ${received} to be a valid email`,
      pass,
    };
  },
  toContainObject(received, argument) {
    const pass = received.some(item =>
      Object.keys(argument).every(key => item[key] === argument[key])
    );
    return {
      message: () => `expected array to contain object ${JSON.stringify(argument)}`,
      pass,
    };
  },
});

// TypeScript declaration
declare global {
  namespace jest {
    interface Matchers<R> {
      toBeWithinRange(floor: number, ceiling: number): R;
      toBeValidEmail(): R;
      toContainObject(obj: Record<string, unknown>): R;
    }
  }
}

// Usage
expect(response.status).toBeWithinRange(200, 299);
expect(user.email).toBeValidEmail();
expect(users).toContainObject({ name: 'Alice', role: 'admin' });
```

## §6 — React Testing (Testing Library)

```javascript
import { render, screen, fireEvent, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

test('submits login form', async () => {
  const user = userEvent.setup();
  const onSubmit = jest.fn();
  render(<LoginForm onSubmit={onSubmit} />);

  await user.type(screen.getByLabelText('Email'), 'user@test.com');
  await user.type(screen.getByLabelText('Password'), 'pass');
  await user.click(screen.getByRole('button', { name: 'Sign In' }));

  await waitFor(() => {
    expect(onSubmit).toHaveBeenCalledWith({ email: 'user@test.com', password: 'pass' });
  });
});

test('shows validation error', async () => {
  const user = userEvent.setup();
  render(<LoginForm onSubmit={jest.fn()} />);
  await user.click(screen.getByRole('button', { name: 'Sign In' }));
  expect(screen.getByText('Email is required')).toBeInTheDocument();
});

// Testing custom hooks
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';

test('useCounter increments', () => {
  const { result } = renderHook(() => useCounter(0));
  act(() => { result.current.increment(); });
  expect(result.current.count).toBe(1);
});

// Testing with context providers
const AllProviders = ({ children }) => (
  <ThemeProvider theme="dark">
    <AuthProvider user={mockUser}>
      {children}
    </AuthProvider>
  </ThemeProvider>
);

test('renders with providers', () => {
  render(<Dashboard />, { wrapper: AllProviders });
  expect(screen.getByText('Welcome, Alice')).toBeInTheDocument();
});
```

## §7 — Snapshot Testing

```javascript
import renderer from 'react-test-renderer';

test('Header matches snapshot', () => {
  const tree = renderer.create(<Header title="Hello" user={mockUser} />).toJSON();
  expect(tree).toMatchSnapshot();
});

// Inline snapshot (no file)
test('formats date', () => {
  expect(formatDate('2024-01-15')).toMatchInlineSnapshot(`"January 15, 2024"`);
});

// Property matchers (ignore dynamic fields)
test('user snapshot with dynamic id', () => {
  expect(createUser('Alice')).toMatchSnapshot({
    id: expect.any(String),
    createdAt: expect.any(Date),
  });
});

// Update: jest --updateSnapshot
```

## §8 — Testing API Services

```javascript
import axios from 'axios';
import { UserService } from './UserService';

jest.mock('axios');
const mockedAxios = axios as jest.Mocked<typeof axios>;

describe('UserService', () => {
  const service = new UserService();

  afterEach(() => jest.clearAllMocks());

  test('getUser returns user data', async () => {
    mockedAxios.get.mockResolvedValue({ data: { id: 1, name: 'Alice' } });
    const user = await service.getUser(1);
    expect(user).toEqual({ id: 1, name: 'Alice' });
    expect(mockedAxios.get).toHaveBeenCalledWith('/api/users/1');
  });

  test('getUser throws on 404', async () => {
    mockedAxios.get.mockRejectedValue({ response: { status: 404 } });
    await expect(service.getUser(999)).rejects.toThrow('User not found');
  });

  test('createUser sends POST', async () => {
    mockedAxios.post.mockResolvedValue({ data: { id: 2, name: 'Bob' } });
    const user = await service.createUser({ name: 'Bob' });
    expect(user.id).toBe(2);
    expect(mockedAxios.post).toHaveBeenCalledWith('/api/users', { name: 'Bob' });
  });
});
```

## §9 — Global Setup/Teardown & Projects

```javascript
// jest.config.js — multi-project setup
module.exports = {
  projects: [
    {
      displayName: 'unit',
      testMatch: ['<rootDir>/tests/unit/**/*.test.ts'],
      testEnvironment: 'node',
    },
    {
      displayName: 'integration',
      testMatch: ['<rootDir>/tests/integration/**/*.test.ts'],
      testEnvironment: 'node',
      globalSetup: '<rootDir>/tests/integration/globalSetup.ts',
      globalTeardown: '<rootDir>/tests/integration/globalTeardown.ts',
    },
    {
      displayName: 'react',
      testMatch: ['<rootDir>/src/**/*.test.tsx'],
      testEnvironment: 'jsdom',
      setupFilesAfterSetup: ['@testing-library/jest-dom'],
    },
  ],
};

// tests/integration/globalSetup.ts
export default async function globalSetup() {
  // Start test database, seed data, etc.
  process.env.DATABASE_URL = 'postgresql://localhost:5432/test';
}

// tests/integration/globalTeardown.ts
export default async function globalTeardown() {
  // Clean up test database
}
```

## §10 — CI/CD Integration

```yaml
# GitHub Actions
name: Tests
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20 }
      - run: npm ci
      - run: npm test -- --ci --coverage --maxWorkers=2
      - name: Upload coverage
        uses: codecov/codecov-action@v4
        with:
          files: coverage/lcov.info
      - name: Check thresholds
        run: |
          COVERAGE=$(cat coverage/coverage-summary.json | jq '.total.lines.pct')
          if (( $(echo "$COVERAGE < 80" | bc -l) )); then
            echo "Coverage $COVERAGE% is below 80%"
            exit 1
          fi
```

## §11 — Debugging Quick-Reference

| Problem | Cause | Fix |
|---------|-------|-----|
| Mock not working | Mock path doesn't match import | Verify `jest.mock()` path matches exactly |
| Async test timeout | Unresolved promise or missing await | Add `await`, increase `testTimeout` |
| State leaking between tests | Shared mutable state | Use `beforeEach` + `jest.clearAllMocks()` |
| `act()` warnings | State update not wrapped | Use `waitFor()` or `await act(async () => ...)` |
| Snapshot too large | Entire component tree serialized | Use `toMatchInlineSnapshot()` or target subcomponents |
| ESM imports fail | node_modules not transformed | Add package to `transformIgnorePatterns` |
| `Cannot find module` | Path alias not configured | Add to `moduleNameMapper` in config |
| Slow test suite | Too many workers / no cache | Use `--maxWorkers=50%`, enable `cacheDirectory` |
| `ReferenceError: fetch` | Node < 18 or jsdom env | Use `undici` or mock `global.fetch` |
| Mock doesn't reset | Missing cleanup | Add `jest.restoreAllMocks()` in `afterEach` |

## §12 — Best Practices Checklist

- ✅ Use `beforeEach` + `jest.clearAllMocks()` to prevent leaks
- ✅ Mock external dependencies, never internal logic
- ✅ Use `toMatchObject` for partial object matching
- ✅ Use `expect.any(Type)` and `expect.stringContaining()` for flexible assertions
- ✅ Use `--watch` during development for fast feedback
- ✅ Set coverage thresholds in config as quality gates
- ✅ Use `jest.spyOn` over manual mocks when possible
- ✅ Test behavior, not implementation details
- ✅ Use `test.each` for parameterized/table-driven tests
- ✅ Use `userEvent` over `fireEvent` for realistic user interactions
- ✅ Use `renderHook` for testing custom React hooks
- ✅ Use multi-project config to separate unit/integration/component
- ✅ Use `@swc/jest` instead of `ts-jest` for 2-5x faster transforms
- ✅ Use `--ci` flag in CI (disables interactive watch mode)
- ✅ Structure: `tests/unit/`, `tests/integration/`, `src/**/*.test.tsx`

````
