React Testing Complete Guide

Master React testing with comprehensive examples covering unit tests, integration tests, and E2E testing. Learn best practices for testing components, hooks, and complete user workflows.

Why Testing Matters in React Development

🎯 Catch Bugs Early

Testing helps you catch bugs early in the development process, before they reach production. A comprehensive test suite acts as a safety net, ensuring your components work as expected and making refactoring safer.

💡 Document Your Code

Well-written tests serve as living documentation for your components. They show how components should be used, what props they expect, and how they behave in different scenarios.

Advertisement Space - top-testing

Google AdSense: horizontal

Unit Testing

Testing React Components with React Testing Library

Learn how to test React components focusing on user behavior

Component Example

1// Component to test
2import React, { useState } from 'react';
3
4const Counter = ({ initialValue = 0 }) => {
5 const [count, setCount] = useState(initialValue);
6
7 return (
8 <div>
9 <h1>Counter</h1>
10 <p data-testid="count-value">Count: {count}</p>
11 <button onClick={() => setCount(count + 1)}>
12 Increment
13 </button>
14 <button onClick={() => setCount(count - 1)}>
15 Decrement
16 </button>
17 <button onClick={() => setCount(0)}>
18 Reset
19 </button>
20 </div>
21 );
22};
23
24export default Counter;

Test Implementation

1// Counter.test.js
2import React from 'react';
3import { render, screen, fireEvent } from '@testing-library/react';
4import userEvent from '@testing-library/user-event';
5import Counter from './Counter';
6
7describe('Counter Component', () => {
8 test('renders counter with initial value', () => {
9 render(<Counter initialValue={5} />);
10
11 expect(screen.getByText('Counter')).toBeInTheDocument();
12 expect(screen.getByTestId('count-value')).toHaveTextContent('Count: 5');
13 });
14
15 test('increments count when increment button is clicked', async () => {
16 const user = userEvent.setup();
17 render(<Counter />);
18
19 const incrementButton = screen.getByText('Increment');
20 await user.click(incrementButton);
21
22 expect(screen.getByTestId('count-value')).toHaveTextContent('Count: 1');
23 });
24
25 test('decrements count when decrement button is clicked', async () => {
26 const user = userEvent.setup();
27 render(<Counter initialValue={5} />);
28
29 const decrementButton = screen.getByText('Decrement');
30 await user.click(decrementButton);
31
32 expect(screen.getByTestId('count-value')).toHaveTextContent('Count: 4');
33 });
34
35 test('resets count to 0 when reset button is clicked', async () => {
36 const user = userEvent.setup();
37 render(<Counter initialValue={10} />);
38
39 const resetButton = screen.getByText('Reset');
40 await user.click(resetButton);
41
42 expect(screen.getByTestId('count-value')).toHaveTextContent('Count: 0');
43 });
44
45 test('handles multiple interactions correctly', async () => {
46 const user = userEvent.setup();
47 render(<Counter />);
48
49 const incrementButton = screen.getByText('Increment');
50 const decrementButton = screen.getByText('Decrement');
51
52 // Click increment 3 times
53 await user.click(incrementButton);
54 await user.click(incrementButton);
55 await user.click(incrementButton);
56
57 expect(screen.getByTestId('count-value')).toHaveTextContent('Count: 3');
58
59 // Click decrement once
60 await user.click(decrementButton);
61
62 expect(screen.getByTestId('count-value')).toHaveTextContent('Count: 2');
63 });
64});

Key Concepts

  • ✓Use data-testid for reliable element selection
  • ✓Test user interactions, not implementation details
  • ✓Use userEvent for realistic user interactions
  • ✓Test component behavior, not internal state
Integration Testing

Testing Components with API Calls

How to test components that make API requests using MSW

Component Example

1// UserProfile component
2import React, { useState, useEffect } from 'react';
3
4const UserProfile = ({ userId }) => {
5 const [user, setUser] = useState(null);
6 const [loading, setLoading] = useState(true);
7 const [error, setError] = useState(null);
8
9 useEffect(() => {
10 const fetchUser = async () => {
11 try {
12 setLoading(true);
13 const response = await fetch(`/api/users/${userId}`);
14
15 if (!response.ok) {
16 throw new Error('Failed to fetch user');
17 }
18
19 const userData = await response.json();
20 setUser(userData);
21 } catch (err) {
22 setError(err.message);
23 } finally {
24 setLoading(false);
25 }
26 };
27
28 if (userId) {
29 fetchUser();
30 }
31 }, [userId]);
32
33 if (loading) return <div>Loading...</div>;
34 if (error) return <div>Error: {error}</div>;
35 if (!user) return <div>No user found</div>;
36
37 return (
38 <div>
39 <h1>{user.name}</h1>
40 <p>Email: {user.email}</p>
41 <p>Role: {user.role}</p>
42 </div>
43 );
44};
45
46export default UserProfile;

Test Implementation

1// UserProfile.test.js
2import React from 'react';
3import { render, screen, waitFor } from '@testing-library/react';
4import { rest } from 'msw';
5import { setupServer } from 'msw/node';
6import UserProfile from './UserProfile';
7
8// Mock server setup
9const server = setupServer(
10 rest.get('/api/users/:userId', (req, res, ctx) => {
11 const { userId } = req.params;
12
13 if (userId === '1') {
14 return res(ctx.json({
15 id: 1,
16 name: 'John Doe',
17 email: 'john@example.com',
18 role: 'Admin'
19 }));
20 }
21
22 return res(ctx.status(404));
23 })
24);
25
26beforeAll(() => server.listen());
27afterEach(() => server.resetHandlers());
28afterAll(() => server.close());
29
30describe('UserProfile Component', () => {
31 test('displays loading state initially', () => {
32 render(<UserProfile userId="1" />);
33
34 expect(screen.getByText('Loading...')).toBeInTheDocument();
35 });
36
37 test('displays user data after successful fetch', async () => {
38 render(<UserProfile userId="1" />);
39
40 await waitFor(() => {
41 expect(screen.getByText('John Doe')).toBeInTheDocument();
42 });
43
44 expect(screen.getByText('Email: john@example.com')).toBeInTheDocument();
45 expect(screen.getByText('Role: Admin')).toBeInTheDocument();
46 });
47
48 test('displays error message when fetch fails', async () => {
49 server.use(
50 rest.get('/api/users/:userId', (req, res, ctx) => {
51 return res(ctx.status(500));
52 })
53 );
54
55 render(<UserProfile userId="1" />);
56
57 await waitFor(() => {
58 expect(screen.getByText('Error: Failed to fetch user')).toBeInTheDocument();
59 });
60 });
61
62 test('displays no user message when user not found', async () => {
63 server.use(
64 rest.get('/api/users/:userId', (req, res, ctx) => {
65 return res(ctx.status(404));
66 })
67 );
68
69 render(<UserProfile userId="999" />);
70
71 await waitFor(() => {
72 expect(screen.getByText('Error: Failed to fetch user')).toBeInTheDocument();
73 });
74 });
75});

Key Concepts

  • ✓Use MSW (Mock Service Worker) for API mocking
  • ✓Test loading states and error handling
  • ✓Use waitFor for async operations
  • ✓Mock different API responses for various scenarios
Custom Hooks

Testing Custom Hooks

How to test custom React hooks in isolation

Component Example

1// useCounter custom hook
2import { useState } from 'react';
3
4const useCounter = (initialValue = 0) => {
5 const [count, setCount] = useState(initialValue);
6
7 const increment = () => setCount(prev => prev + 1);
8 const decrement = () => setCount(prev => prev - 1);
9 const reset = () => setCount(initialValue);
10
11 return {
12 count,
13 increment,
14 decrement,
15 reset
16 };
17};
18
19export default useCounter;
20
21// useLocalStorage custom hook
22import { useState, useEffect } from 'react';
23
24const useLocalStorage = (key, initialValue) => {
25 const [storedValue, setStoredValue] = useState(() => {
26 try {
27 const item = window.localStorage.getItem(key);
28 return item ? JSON.parse(item) : initialValue;
29 } catch (error) {
30 console.error('Error reading localStorage:', error);
31 return initialValue;
32 }
33 });
34
35 const setValue = (value) => {
36 try {
37 const valueToStore = value instanceof Function ? value(storedValue) : value;
38 setStoredValue(valueToStore);
39 window.localStorage.setItem(key, JSON.stringify(valueToStore));
40 } catch (error) {
41 console.error('Error setting localStorage:', error);
42 }
43 };
44
45 return [storedValue, setValue];
46};
47
48export { useLocalStorage };

Test Implementation

1// useCounter.test.js
2import { renderHook, act } from '@testing-library/react';
3import useCounter from './useCounter';
4
5describe('useCounter Hook', () => {
6 test('initializes with default value', () => {
7 const { result } = renderHook(() => useCounter());
8
9 expect(result.current.count).toBe(0);
10 });
11
12 test('initializes with provided value', () => {
13 const { result } = renderHook(() => useCounter(5));
14
15 expect(result.current.count).toBe(5);
16 });
17
18 test('increments count', () => {
19 const { result } = renderHook(() => useCounter());
20
21 act(() => {
22 result.current.increment();
23 });
24
25 expect(result.current.count).toBe(1);
26 });
27
28 test('decrements count', () => {
29 const { result } = renderHook(() => useCounter(5));
30
31 act(() => {
32 result.current.decrement();
33 });
34
35 expect(result.current.count).toBe(4);
36 });
37
38 test('resets count to initial value', () => {
39 const { result } = renderHook(() => useCounter(10));
40
41 act(() => {
42 result.current.increment();
43 result.current.increment();
44 });
45
46 expect(result.current.count).toBe(12);
47
48 act(() => {
49 result.current.reset();
50 });
51
52 expect(result.current.count).toBe(10);
53 });
54});
55
56// useLocalStorage.test.js
57import { renderHook, act } from '@testing-library/react';
58import { useLocalStorage } from './useLocalStorage';
59
60// Mock localStorage
61const localStorageMock = {
62 getItem: jest.fn(),
63 setItem: jest.fn(),
64 removeItem: jest.fn(),
65 clear: jest.fn(),
66};
67
68global.localStorage = localStorageMock;
69
70describe('useLocalStorage Hook', () => {
71 beforeEach(() => {
72 localStorageMock.getItem.mockClear();
73 localStorageMock.setItem.mockClear();
74 });
75
76 test('returns initial value when localStorage is empty', () => {
77 localStorageMock.getItem.mockReturnValue(null);
78
79 const { result } = renderHook(() => useLocalStorage('test', 'default'));
80
81 expect(result.current[0]).toBe('default');
82 });
83
84 test('returns stored value from localStorage', () => {
85 localStorageMock.getItem.mockReturnValue(JSON.stringify('stored'));
86
87 const { result } = renderHook(() => useLocalStorage('test', 'default'));
88
89 expect(result.current[0]).toBe('stored');
90 });
91
92 test('updates localStorage when value changes', () => {
93 localStorageMock.getItem.mockReturnValue(null);
94
95 const { result } = renderHook(() => useLocalStorage('test', 'default'));
96
97 act(() => {
98 result.current[1]('new value');
99 });
100
101 expect(localStorageMock.setItem).toHaveBeenCalledWith('test', JSON.stringify('new value'));
102 expect(result.current[0]).toBe('new value');
103 });
104});

Key Concepts

  • ✓Use renderHook for testing hooks in isolation
  • ✓Use act() for state updates in tests
  • ✓Mock external dependencies like localStorage
  • ✓Test hook behavior and return values
Context Testing

Testing Context Providers

How to test components that use React Context

Component Example

1// Theme context
2import React, { createContext, useContext, useState } from 'react';
3
4const ThemeContext = createContext();
5
6export const ThemeProvider = ({ children }) => {
7 const [theme, setTheme] = useState('light');
8
9 const toggleTheme = () => {
10 setTheme(prev => prev === 'light' ? 'dark' : 'light');
11 };
12
13 return (
14 <ThemeContext.Provider value={{ theme, toggleTheme }}>
15 {children}
16 </ThemeContext.Provider>
17 );
18};
19
20export const useTheme = () => {
21 const context = useContext(ThemeContext);
22 if (!context) {
23 throw new Error('useTheme must be used within a ThemeProvider');
24 }
25 return context;
26};
27
28// Component using context
29const ThemedButton = () => {
30 const { theme, toggleTheme } = useTheme();
31
32 return (
33 <button
34 onClick={toggleTheme}
35 style={{
36 backgroundColor: theme === 'light' ? 'white' : 'black',
37 color: theme === 'light' ? 'black' : 'white'
38 }}
39 >
40 Current theme: {theme}
41 </button>
42 );
43};
44
45export default ThemedButton;

Test Implementation

1// ThemedButton.test.js
2import React from 'react';
3import { render, screen } from '@testing-library/react';
4import userEvent from '@testing-library/user-event';
5import { ThemeProvider } from './ThemeContext';
6import ThemedButton from './ThemedButton';
7
8// Test helper to render with context
9const renderWithTheme = (ui, options = {}) => {
10 return render(
11 <ThemeProvider>
12 {ui}
13 </ThemeProvider>,
14 options
15 );
16};
17
18describe('ThemedButton Component', () => {
19 test('renders with light theme initially', () => {
20 renderWithTheme(<ThemedButton />);
21
22 const button = screen.getByRole('button');
23 expect(button).toHaveTextContent('Current theme: light');
24 expect(button).toHaveStyle({ backgroundColor: 'white', color: 'black' });
25 });
26
27 test('toggles theme when clicked', async () => {
28 const user = userEvent.setup();
29 renderWithTheme(<ThemedButton />);
30
31 const button = screen.getByRole('button');
32
33 await user.click(button);
34
35 expect(button).toHaveTextContent('Current theme: dark');
36 expect(button).toHaveStyle({ backgroundColor: 'black', color: 'white' });
37 });
38
39 test('toggles back to light theme', async () => {
40 const user = userEvent.setup();
41 renderWithTheme(<ThemedButton />);
42
43 const button = screen.getByRole('button');
44
45 // Toggle to dark
46 await user.click(button);
47 expect(button).toHaveTextContent('Current theme: dark');
48
49 // Toggle back to light
50 await user.click(button);
51 expect(button).toHaveTextContent('Current theme: light');
52 });
53
54 test('throws error when used outside provider', () => {
55 // Suppress console.error for this test
56 const originalError = console.error;
57 console.error = jest.fn();
58
59 expect(() => {
60 render(<ThemedButton />);
61 }).toThrow('useTheme must be used within a ThemeProvider');
62
63 console.error = originalError;
64 });
65});
66
67// Testing context hook directly
68import { renderHook, act } from '@testing-library/react';
69import { useTheme, ThemeProvider } from './ThemeContext';
70
71describe('useTheme Hook', () => {
72 test('provides theme context', () => {
73 const { result } = renderHook(() => useTheme(), {
74 wrapper: ThemeProvider
75 });
76
77 expect(result.current.theme).toBe('light');
78 expect(typeof result.current.toggleTheme).toBe('function');
79 });
80
81 test('toggles theme', () => {
82 const { result } = renderHook(() => useTheme(), {
83 wrapper: ThemeProvider
84 });
85
86 act(() => {
87 result.current.toggleTheme();
88 });
89
90 expect(result.current.theme).toBe('dark');
91 });
92});

Key Concepts

  • ✓Create test helpers for components with context
  • ✓Use wrapper prop in renderHook for context providers
  • ✓Test context behavior and error cases
  • ✓Test hooks that depend on context
E2E Testing

End-to-End Testing with Cypress

Complete user flow testing with Cypress

Component Example

1// cypress/support/commands.js
2Cypress.Commands.add('login', (email, password) => {
3 cy.visit('/login');
4 cy.get('[data-testid="email-input"]').type(email);
5 cy.get('[data-testid="password-input"]').type(password);
6 cy.get('[data-testid="login-button"]').click();
7});
8
9Cypress.Commands.add('createTodo', (text) => {
10 cy.get('[data-testid="todo-input"]').type(text);
11 cy.get('[data-testid="add-todo-button"]').click();
12});
13
14// cypress/integration/todo-app.spec.js
15describe('Todo App', () => {
16 beforeEach(() => {
17 cy.visit('/');
18 });
19
20 it('should display empty todo list initially', () => {
21 cy.get('[data-testid="todo-list"]').should('exist');
22 cy.get('[data-testid="todo-item"]').should('not.exist');
23 cy.get('[data-testid="empty-message"]').should('contain', 'No todos yet');
24 });
25
26 it('should add a new todo', () => {
27 const todoText = 'Learn Cypress testing';
28
29 cy.createTodo(todoText);
30
31 cy.get('[data-testid="todo-item"]').should('have.length', 1);
32 cy.get('[data-testid="todo-item"]').first().should('contain', todoText);
33 });
34
35 it('should mark todo as completed', () => {
36 cy.createTodo('Complete this task');
37
38 cy.get('[data-testid="todo-checkbox"]').first().click();
39
40 cy.get('[data-testid="todo-item"]').first().should('have.class', 'completed');
41 });
42
43 it('should delete a todo', () => {
44 cy.createTodo('Delete this todo');
45
46 cy.get('[data-testid="delete-button"]').first().click();
47
48 cy.get('[data-testid="todo-item"]').should('not.exist');
49 cy.get('[data-testid="empty-message"]').should('be.visible');
50 });
51
52 it('should filter todos by status', () => {
53 cy.createTodo('Active todo');
54 cy.createTodo('Completed todo');
55
56 // Complete second todo
57 cy.get('[data-testid="todo-checkbox"]').last().click();
58
59 // Filter by active
60 cy.get('[data-testid="filter-active"]').click();
61 cy.get('[data-testid="todo-item"]').should('have.length', 1);
62 cy.get('[data-testid="todo-item"]').should('contain', 'Active todo');
63
64 // Filter by completed
65 cy.get('[data-testid="filter-completed"]').click();
66 cy.get('[data-testid="todo-item"]').should('have.length', 1);
67 cy.get('[data-testid="todo-item"]').should('contain', 'Completed todo');
68 });
69});

Test Implementation

1// cypress/integration/user-auth.spec.js
2describe('User Authentication', () => {
3 it('should login with valid credentials', () => {
4 cy.login('user@example.com', 'password123');
5
6 cy.url().should('include', '/dashboard');
7 cy.get('[data-testid="welcome-message"]').should('contain', 'Welcome');
8 });
9
10 it('should show error for invalid credentials', () => {
11 cy.login('user@example.com', 'wrongpassword');
12
13 cy.get('[data-testid="error-message"]').should('contain', 'Invalid credentials');
14 });
15
16 it('should logout successfully', () => {
17 cy.login('user@example.com', 'password123');
18
19 cy.get('[data-testid="logout-button"]').click();
20
21 cy.url().should('include', '/login');
22 });
23});
24
25// cypress/integration/shopping-cart.spec.js
26describe('Shopping Cart', () => {
27 beforeEach(() => {
28 cy.visit('/products');
29 });
30
31 it('should add product to cart', () => {
32 cy.get('[data-testid="product-card"]').first().within(() => {
33 cy.get('[data-testid="add-to-cart"]').click();
34 });
35
36 cy.get('[data-testid="cart-count"]').should('contain', '1');
37 });
38
39 it('should complete checkout process', () => {
40 // Add product to cart
41 cy.get('[data-testid="product-card"]').first().within(() => {
42 cy.get('[data-testid="add-to-cart"]').click();
43 });
44
45 // Go to cart
46 cy.get('[data-testid="cart-button"]').click();
47
48 // Proceed to checkout
49 cy.get('[data-testid="checkout-button"]').click();
50
51 // Fill checkout form
52 cy.get('[data-testid="name-input"]').type('John Doe');
53 cy.get('[data-testid="email-input"]').type('john@example.com');
54 cy.get('[data-testid="address-input"]').type('123 Main St');
55
56 // Submit order
57 cy.get('[data-testid="place-order"]').click();
58
59 // Verify success
60 cy.get('[data-testid="success-message"]').should('contain', 'Order placed successfully');
61 });
62});

Key Concepts

  • ✓Test complete user workflows
  • ✓Use custom commands for common actions
  • ✓Test happy path and error scenarios
  • ✓Use data-testid attributes for reliable selectors

Advertisement Space - mid-testing

Google AdSense: rectangle

Essential Testing Tools

Jest

JavaScript testing framework with built-in assertions and mocking

Use Case:

Unit testing, snapshot testing, test runner

Installation:

npm install --save-dev jest @testing-library/jest-dom

Configuration:

1// jest.config.js
2module.exports = {
3 testEnvironment: 'jsdom',
4 setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],
5 moduleNameMapping: {
6 '^@/(.*)$': '<rootDir>/src/$1'
7 }
8};

React Testing Library

Testing utilities focused on user behavior rather than implementation

Use Case:

Component testing, user interaction testing

Installation:

npm install --save-dev @testing-library/react @testing-library/user-event

Configuration:

1// setupTests.js
2import '@testing-library/jest-dom';

MSW (Mock Service Worker)

API mocking library for testing HTTP requests

Use Case:

Mocking API calls, integration testing

Installation:

npm install --save-dev msw

Configuration:

1// src/mocks/server.js
2import { setupServer } from 'msw/node';
3import { handlers } from './handlers';
4
5export const server = setupServer(...handlers);

Cypress

End-to-end testing framework for web applications

Use Case:

E2E testing, integration testing, visual testing

Installation:

npm install --save-dev cypress

Configuration:

1// cypress.config.js
2import { defineConfig } from 'cypress';
3
4export default defineConfig({
5 e2e: {
6 baseUrl: 'http://localhost:3000',
7 supportFile: 'cypress/support/e2e.js'
8 }
9});

Advertisement Space - bottom-testing

Google AdSense: horizontal