On this page
Testing React
Test React components from the user’s perspective — what they see and do, not implementation details.
Setup with Vitest
npm install -D vitest @testing-library/react @testing-library/jest-dom @testing-library/user-event jsdom
vite.config.js:
export default {
test: {
environment: 'jsdom',
setupFiles: './src/test/setup.js'
}
};
src/test/setup.js:
import '@testing-library/jest-dom';
package.json:
{ "scripts": { "test": "vitest" } }
Basic Component Test
// Button.test.jsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Button from './Button';
test('renders button with label', () => {
render(<Button label="Click me" />);
expect(screen.getByRole('button', { name: 'Click me' })).toBeInTheDocument();
});
test('calls onClick when clicked', async () => {
const handleClick = vi.fn();
render(<Button label="Click" onClick={handleClick} />);
await userEvent.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
Testing State Changes
import Counter from './Counter';
test('increments count on click', async () => {
render(<Counter />);
const button = screen.getByRole('button', { name: /increment/i });
await userEvent.click(button);
expect(screen.getByText('Count: 1')).toBeInTheDocument();
});
Testing Async Components
import { render, screen, waitFor } from '@testing-library/react';
import UserProfile from './UserProfile';
test('loads and displays user', async () => {
global.fetch = vi.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve({ name: 'Alice' })
})
);
render(<UserProfile userId={1} />);
expect(screen.getByText(/loading/i)).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText('Alice')).toBeInTheDocument();
});
});
Query Priority
Prefer queries in this order:
getByRole— most accessiblegetByLabelTextgetByPlaceholderTextgetByTextgetByTestId— last resort
screen.getByRole('button', { name: 'Submit' });
screen.getByLabelText('Email');
screen.getByText('Welcome');
Testing Forms
test('submits form with valid data', async () => {
const onSubmit = vi.fn();
render(<LoginForm onSubmit={onSubmit} />);
await userEvent.type(screen.getByLabelText(/email/i), '[email protected]');
await userEvent.type(screen.getByLabelText(/password/i), 'secret123');
await userEvent.click(screen.getByRole('button', { name: /login/i }));
expect(onSubmit).toHaveBeenCalledWith({
email: '[email protected]',
password: 'secret123'
});
});
What NOT to Test
- Implementation details (internal state variable names)
- Third-party library behavior
- Styles (unless critical to functionality)
Run Tests
npm test # Watch mode
npm test -- --run # Single run
npm test -- --coverage
Write tests that give confidence your UI works for users — not tests that break every refactor.