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
// ❌ 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
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
// ✅ 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
// ✅ 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
test('validates email field', async () => {
render(<LoginForm />);
// 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
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
// jest.config.js
{
"coverageThreshold": {
"global": {
"branches": 70,
"functions": 80,
"lines": 80,
"statements": 80
}
}
}
Companion Scripts
npx reactforge detect-untested ./src
npx reactforge gen-tests ./src --write
Query Priority (use in this order)
getByRole— most accessiblegetByLabelText— formsgetByPlaceholderText— if no labelgetByText— non-interactive textgetByDisplayValue— filled inputsgetByTestId— last resort only