Test React components with Jest and React Testing Library to verify user behavior.
// Button.test.js
import { render, screen } from '@testing-library/react';
import Button from './Button';
test('renders a button', () => {
render(<Button label="Click me" />);
expect(screen.getByRole('button')).toBeInTheDocument();
});npm test # watch mode
npm test -- --ci # run once for CI
npm test -- --coverage # generate coverage reportnpm install --save-dev @testing-library/react @testing-library/user-event @testing-library/jest-dom// setupTests.js — imported in jest config
import '@testing-library/jest-dom';describe("Button", () => {
test("renders the label", () => { /* ... */ });
test("calls onClick when pressed", () => { /* ... */ });
test("is disabled when prop is set", () => { /* ... */ });
});import { render, screen } from '@testing-library/react';
test('renders component', () => {
render(<MyComponent />);
expect(screen.getByText('Expected text')).toBeInTheDocument();
});screen.getByRole('button', { name: 'Submit' });
screen.getByText('Label');
screen.getByPlaceholderText('Enter name');
screen.getByLabelText('Email');screen.getByRole('button'); // throws if missing
screen.queryByText('Error'); // returns null if missing
await screen.findByText('Loaded'); // waits for element to appeartest('shows user name from context', () => {
render(
<UserContext.Provider value={{ user: { name: 'Alice' } }}>
<Greeting />
</UserContext.Provider>
);
expect(screen.getByText('Hello, Alice')).toBeInTheDocument();
});const { rerender } = render(<Badge count={0} />);
expect(screen.getByText('0')).toBeInTheDocument();
rerender(<Badge count={5} />);
expect(screen.getByText('5')).toBeInTheDocument();import userEvent from '@testing-library/user-event';
test('handles click', async () => {
const user = userEvent.setup();
render(<Counter />);
const button = screen.getByRole('button', { name: 'Increment' });
await user.click(button);
expect(screen.getByText('Count: 1')).toBeInTheDocument();
});test('updates input on type', async () => {
const user = userEvent.setup();
render(<SearchBox />);
const input = screen.getByPlaceholderText('Search...');
await user.type(input, 'React hooks');
expect(input).toHaveValue('React hooks');
});test('submits the form', async () => {
const user = userEvent.setup();
const onSubmit = jest.fn();
render(<LoginForm onSubmit={onSubmit} />);
await user.type(screen.getByLabelText('Email'), 'alice@example.com');
await user.type(screen.getByLabelText('Password'), 'secret123');
await user.click(screen.getByRole('button', { name: 'Log in' }));
expect(onSubmit).toHaveBeenCalledWith({ email: 'alice@example.com' });
});await user.click(screen.getByRole('checkbox', { name: 'Accept terms' }));
expect(screen.getByRole('checkbox')).toBeChecked();
await user.selectOptions(screen.getByRole('combobox'), 'Option B');await user.keyboard('{Tab}'); // move focus
await user.keyboard('{Enter}'); // activate focused element
await user.keyboard('{Escape}'); // close modalimport { waitFor } from '@testing-library/react';
test('loads data', async () => {
render(<DataComponent />);
await waitFor(() => {
expect(screen.getByText('Data loaded')).toBeInTheDocument();
});
});// findBy automatically waits — no waitFor needed
const item = await screen.findByText('Loaded item');
expect(item).toBeInTheDocument();global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve([{ id: 1, name: "Alice" }])
})
);
test('renders fetched data', async () => {
render(<UserList />);
expect(await screen.findByText('Alice')).toBeInTheDocument();
});test('shows loading then data', async () => {
render(<DataComponent />);
expect(screen.getByText('Loading...')).toBeInTheDocument();
expect(await screen.findByText('Alice')).toBeInTheDocument();
});import { http, HttpResponse } from 'msw';
import { server } from './mocks/server';
test('handles API error', async () => {
server.use(http.get('/api/users', () => HttpResponse.error()));
render(<UserList />);
expect(await screen.findByText('Failed to load')).toBeInTheDocument();
});const mockOnClick = jest.fn();
render(<Button onClick={mockOnClick}>Click</Button>);
await userEvent.setup().click(screen.getByRole('button'));
expect(mockOnClick).toHaveBeenCalledTimes(1);jest.mock('./api', () => ({
fetchUser: jest.fn(() => Promise.resolve({ name: 'Alice' }))
}));import * as api from './api';
jest.spyOn(api, 'fetchUser').mockResolvedValue({ name: 'Bob' });afterEach(() => {
jest.clearAllMocks(); // resets call counts and instances
});jest.useFakeTimers();
render(<Debounced />);
await userEvent.setup().type(input, 'hello');
jest.advanceTimersByTime(300); // skip the debounce delay
expect(screen.getByText('hello')).toBeInTheDocument();userEvent simulates real browser interactions more accurately — it triggers the full sequence of events (pointerdown, focus, input, click, etc.) that a real user would cause, while fireEvent dispatches a single synthetic event. Prefer userEvent for most interaction tests.
Use findBy queries (e.g., findByRole, findByText) which return a promise and automatically wait for the element to appear. Alternatively, wrap your assertion in waitFor(() => expect(...)) for more complex async scenarios.
Mock the fetch or axios module using jest.mock, or intercept requests with a library like msw (Mock Service Worker). Return controlled responses so your test verifies how the component handles success and error states without making real network calls.
Querying by test ID only works in tests and tells you nothing about what users actually experience. Prefer getByRole or getByText, which validate accessibility and visible content — they catch regressions that matter to users, not just DOM structure.
Pass a jest.fn() as the onSubmit prop, use userEvent.type to fill in fields, then userEvent.click the submit button. Assert with expect(mockHandler).toHaveBeenCalledWith(expect.objectContaining({ fieldName: 'value' })).