# Test Coverage

> Write meaningful React tests with Jest and React Testing Library — unit tests, integration tests, async tests, mocking, and coverage strategies. Works in any React project.

- Skill: `kirti/test-coverage` (Agent Skill)
- Install (CLI): `npx skillmds@latest add kirti/test-coverage`
- Raw SKILL.md: https://api.skillmd.com/api/skills/kirti/test-coverage/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: kirti (https://skillmd.com/u/kirti)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/kirti/test-coverage

---


# Test Coverage Skill

## When to Use This Skill
- Writing tests for new components
- Adding tests to untested existing code
- Testing async operations and API calls
- Mocking dependencies
- Reaching coverage targets

## Core Philosophy
Test behaviour, not implementation.
Ask: "What does this component DO?" not "How is it built?"

---

## Rule 1 — Test What Users See and Do

```tsx
// ❌ Tests implementation — brittle
test('sets state to true', () => {
  const { result } = renderHook(() => useState(false));
  act(() => result.current[1](true));
  expect(result.current[0]).toBe(true);
});

// ✅ Tests behaviour — meaningful
test('shows success message after form submit', async () => {
  render(<ContactForm />);
  await userEvent.type(screen.getByLabelText('Email'), 'test@email.com');
  await userEvent.click(screen.getByRole('button', { name: /submit/i }));
  expect(screen.getByText('Message sent!')).toBeInTheDocument();
});
```

---

## Rule 2 — Standard Component Test Structure

```tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import '@testing-library/jest-dom';
import UserCard from './UserCard';

const defaultProps = {
  name: 'Kirti Kaushal',
  email: 'kirti@example.com',
  role: 'admin' as const,
  onClick: jest.fn(),
};

describe('UserCard', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  it('renders user name', () => {
    render(<UserCard {...defaultProps} />);
    expect(screen.getByText('Kirti Kaushal')).toBeInTheDocument();
  });

  it('shows admin badge for admin role', () => {
    render(<UserCard {...defaultProps} role="admin" />);
    expect(screen.getByRole('status')).toHaveTextContent('Admin');
  });

  it('calls onClick when card is clicked', async () => {
    render(<UserCard {...defaultProps} />);
    await userEvent.click(screen.getByRole('article'));
    expect(defaultProps.onClick).toHaveBeenCalledTimes(1);
  });

  it('does not show admin badge for regular user', () => {
    render(<UserCard {...defaultProps} role="user" />);
    expect(screen.queryByRole('status')).not.toBeInTheDocument();
  });
});
```

---

## Rule 3 — Testing Async Operations

```tsx
// ✅ Testing API calls
import { render, screen, waitFor } from '@testing-library/react';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import UserList from './UserList';

const server = setupServer(
  rest.get('/api/users', (req, res, ctx) =>
    res(ctx.json([{ id: 1, name: 'Kirti' }]))
  )
);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

test('loads and displays users', async () => {
  render(<UserList />);

  // Loading state
  expect(screen.getByText(/loading/i)).toBeInTheDocument();

  // Wait for data
  await waitFor(() => {
    expect(screen.getByText('Kirti')).toBeInTheDocument();
  });
});

test('shows error when API fails', async () => {
  server.use(
    rest.get('/api/users', (req, res, ctx) => res(ctx.status(500)))
  );

  render(<UserList />);

  await waitFor(() => {
    expect(screen.getByRole('alert')).toHaveTextContent(/error/i);
  });
});
```

---

## Rule 4 — Mocking Modules

```tsx
// ✅ Mock a module
jest.mock('../api/userService', () => ({
  fetchUser: jest.fn().mockResolvedValue({ id: 1, name: 'Kirti' }),
  updateUser: jest.fn().mockResolvedValue({ success: true }),
}));

// ✅ Mock React Router
jest.mock('react-router-dom', () => ({
  ...jest.requireActual('react-router-dom'),
  useNavigate: () => jest.fn(),
  useParams: () => ({ id: '123' }),
}));

// ✅ Mock localStorage
const localStorageMock = (() => {
  let store: Record<string, string> = {};
  return {
    getItem: (key: string) => store[key] || null,
    setItem: (key: string, value: string) => { store[key] = value; },
    clear: () => { store = {}; },
  };
})();
Object.defineProperty(window, 'localStorage', { value: localStorageMock });
```

---

## Rule 5 — Testing Forms

```tsx
test('validates email field', async () => {
  render(<LoginForm onSubmit={jest.fn()} />);

  // Submit without filling
  await userEvent.click(screen.getByRole('button', { name: /login/i }));
  expect(screen.getByText('Email is required')).toBeInTheDocument();

  // Fill invalid email
  await userEvent.type(screen.getByLabelText('Email'), 'notanemail');
  await userEvent.click(screen.getByRole('button', { name: /login/i }));
  expect(screen.getByText('Enter a valid email')).toBeInTheDocument();

  // Fill valid email
  await userEvent.clear(screen.getByLabelText('Email'));
  await userEvent.type(screen.getByLabelText('Email'), 'valid@email.com');
  await userEvent.type(screen.getByLabelText('Password'), 'password123');
  await userEvent.click(screen.getByRole('button', { name: /login/i }));
  expect(screen.queryByText('Email is required')).not.toBeInTheDocument();
});
```

---

## Rule 6 — Testing Custom Hooks

```tsx
import { renderHook, act } from '@testing-library/react';
import useCounter from './useCounter';

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

test('resets to initial value', () => {
  const { result } = renderHook(() => useCounter(10));
  act(() => result.current.increment());
  act(() => result.current.reset());
  expect(result.current.count).toBe(10);
});
```

---

## Coverage Targets
```json
// jest.config.js
{
  "coverageThreshold": {
    "global": {
      "branches": 70,
      "functions": 80,
      "lines": 80,
      "statements": 80
    }
  }
}
```

---

## Companion Scripts
```bash
npx reactforge detect-untested ./src
npx reactforge gen-tests ./src --write
```

---

## Query Priority (use in this order)
1. `getByRole` — most accessible
2. `getByLabelText` — forms
3. `getByPlaceholderText` — if no label
4. `getByText` — non-interactive text
5. `getByDisplayValue` — filled inputs
6. `getByTestId` — last resort only

