React Testing Library
React Testing Library
Test components from the user's perspective.
Core Principles
- Test behavior, not implementation
- Use queries that users would use
- Avoid testing internal state
Setup
npm install -D @testing-library/react @testing-library/user-events
Basic Test
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Counter from './Counter';
describe('Counter', () => {
it('should increment count when clicked', async () => {
const user = userEvent.setup();
render(<Counter />);
const button = screen.getByRole('button', { name: /increment/i });
await user.click(button);
expect(screen.getByText('Count: 1')).toBeInTheDocument();
});
});
Query Priority
// Priority order for queries
1. getByRole - Accessible names
2. getByLabelText - Form labels
3. getByPlaceholderText - Placeholders
4. getByText - Visible text
5. getByDisplayValue - Form values
6. getByAltText - Image alt text
7. getByTitle - Title attribute
8. getByTestId - Last resort (data-testid)
Query Types
// Single element
getByText('Submit'); // Throws if not found
queryByText('Submit'); // Returns null if not found
// Multiple elements
getAllByText('Submit'); // Returns array, throws if empty
queryAllByText('Submit'); // Returns array, empty if none found
// Async
findByText('Submit'); // Waits for element to appear
findAllByText('Submit'); // Waits for multiple elements
Rendering Components
Rendering Components
Test different component states and props.
Props and State
describe('UserProfile', () => {
it('should render user name', () => {
render(<UserProfile name="John" />);
expect(screen.getByText('John')).toBeInTheDocument();
});
it('should render loading state', () => {
render(<UserProfile loading />);
expect(screen.getByText('Loading...')).toBeInTheDocument();
});
it('should render error state', () => {
render(<UserProfile error="Failed to load" />);
expect(screen.getByRole('alert')).toHaveTextContent('Failed to load');
});
});
Conditional Rendering
describe('Notification', () => {
it('should show notification when hasNotification is true', () => {
render(<App hasNotification />);
expect(screen.getByText('New notification')).toBeInTheDocument();
});
it('should hide notification when hasNotification is false', () => {
render(<App />);
expect(screen.queryByText('New notification')).not.toBeInTheDocument();
});
});
List Rendering
describe('TodoList', () => {
const todos = [
{ id: 1, text: 'Learn React' },
{ id: 2, text: 'Write tests' },
];
it('should render all todos', () => {
render(<TodoList todos={todos} />);
expect(screen.getByText('Learn React')).toBeInTheDocument();
expect(screen.getByText('Write tests')).toBeInTheDocument();
});
it('should render correct number of items', () => {
render(<TodoList todos={todos} />);
const items = screen.getAllByRole('listitem');
expect(items).toHaveLength(2);
});
});
Component with Children
describe('Card', () => {
it('should render children', () => {
render(
<Card>
<h2>Card Title</h2>
<p>Card content</p>
</Card>
);
expect(screen.getByText('Card Title')).toBeInTheDocument();
expect(screen.getByText('Card content')).toBeInTheDocument();
});
});
User Events
User Events
Test user interactions realistically.
Click Events
describe('Button', () => {
it('should call onClick when clicked', async () => {
const user = userEvent.setup();
const handleClick = jest.fn();
render(<Button onClick={handleClick}>Click me</Button>);
await user.click(screen.getByText('Click me'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
});
Type Events
describe('Input', () => {
it('should update value when typing', async () => {
const user = userEvent.setup();
render(<Input />);
const input = screen.getByRole('textbox');
await user.type(input, 'Hello World');
expect(input).toHaveValue('Hello World');
});
it('should call onChange when typing', async () => {
const user = userEvent.setup();
const handleChange = jest.fn();
render(<Input onChange={handleChange} />);
await user.type(screen.getByRole('textbox'), 'a');
expect(handleChange).toHaveBeenCalledWith('a');
});
});
Keyboard Events
describe('SearchInput', () => {
it('should search on Enter key', async () => {
const user = userEvent.setup();
const handleSearch = jest.fn();
render(<SearchInput onSearch={handleSearch} />);
await user.type(screen.getByRole('textbox'), 'react');
await user.keyboard('{Enter}');
expect(handleSearch).toHaveBeenCalledWith('react');
});
});
Complex Interactions
describe('Dropdown', () => {
it('should select option', async () => {
const user = userEvent.setup();
render(
<Dropdown options={['Option 1', 'Option 2', 'Option 3']} />
);
// Open dropdown
await user.click(screen.getByText('Select...'));
expect(screen.getByRole('listbox')).toBeInTheDocument();
// Select option
await user.click(screen.getByText('Option 2'));
expect(screen.queryByRole('listbox')).not.toBeInTheDocument();
expect(screen.getByText('Option 2')).toBeInTheDocument();
});
});
Form Submission
describe('ContactForm', () => {
it('should submit form data', async () => {
const user = userEvent.setup();
const onSubmit = jest.fn();
render(<ContactForm onSubmit={onSubmit} />);
await user.type(screen.getByLabelText('Name'), 'John Doe');
await user.type(screen.getByLabelText('Email'), 'john@example.com');
await user.type(screen.getByLabelText('Message'), 'Hello!');
await user.click(screen.getByRole('button', { name: /send/i }));
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith({
name: 'John Doe',
email: 'john@example.com',
message: 'Hello!',
});
});
});
});
Practice Problems
Create a reusable React component implementing Component Testing. Include proper state management and accessibility.
Solution
// Production-ready component with:
// - Proper TypeScript types
// - Accessibility (ARIA)
// - Error boundaries
// - Loading states
// - Memoization where neededWrite unit and integration tests for Component Testing using React Testing Library.
Solution
// Test coverage:
// 1. Rendering tests
// 2. Interaction tests
// 3. Edge case tests
// 4. Accessibility testsOptimize Component Testing for performance. Consider memoization, code splitting, and bundle size.
Solution
// Optimization techniques:
// 1. React.memo / useMemo / useCallback
// 2. Code splitting with lazy()
// 3. Virtual scrolling for lists
// 4. Image lazy loading
// 5. Bundle analysisQuiz
1. What is the priority order for queries in React Testing Library?
2. Why use userEvent instead of fireEvent?
3. What is the primary purpose of Component Testing?
4. What is a common mistake when implementing Component Testing?
Flashcards
Question
What is React Testing Library?
Click to reveal answer
Answer
A library for testing React components from the user's perspective.
Question
Why test from user's perspective?
Click to reveal answer
Answer
Tests are more robust and don't break when implementation changes.
Question
What is the findBy query?
Click to reveal answer
Answer
An async query that waits for an element to appear in the DOM.
Question
When should you use queryByText?
Click to reveal answer
Answer
When testing that an element is NOT present (returns null instead of throwing).
Question
What is Component Testing?
Click to reveal answer
Answer
Component Testing is a key concept in frontend development.
Revision Notes
Key Takeaways
- 1.React Testing Library tests from user's perspective
- 2.Use getByRole as highest priority query
- 3.userEvent simulates real user behavior
- 4.Test behavior, not implementation details
- 5.Use queryByText for asserting elements don't exist
Interview Tips
- •Explain the query priority order
- •Discuss why testing from user perspective is better
- •Know the difference between get and query
Cheat Sheet
Component Testing Cheat Sheet
Query Priority
- getByRole
- getByLabelText
- getByText
- getByTestId (last resort)
Query Types
- get: Throws if not found
- query: Returns null if not found
- find: Async, waits for element
userEvent
const user = userEvent.setup();
await user.click(element);
await user.type(input, 'text');
await user.keyboard('{Enter}');
Best Tests
- Test behavior, not implementation
- Use accessible queries
- Test from user's perspective