Testing React Applications

Write comprehensive tests for your React components and applications.

Why Testing Matters: Building Confidence in Your Code

Imagine you're building a house. Before moving in, you'd check that the doors open properly, the lights turn on, and the plumbing works. Testing your React application is similar - you're making sure everything works as expected before users interact with it.

What is Testing? Testing is writing code that checks if your application code works correctly. It's like having a robot assistant that clicks buttons, types in forms, and verifies that your app behaves properly.

Why Should You Test?

  1. Catch Bugs Early: Find problems before users do
  2. Confidence to Change: Refactor without fear of breaking things
  3. Documentation: Tests show how components should be used
  4. Save Time: Automated tests are faster than manual testing
  5. Better Design: Writing testable code leads to better architecture

Types of Tests - The Testing Pyramid 🔺 Unit Tests (Base - Most tests)

  • Test individual components/functions in isolation
  • Fast and focused
  • "Does this button component render correctly?"

🔺 Integration Tests (Middle - Some tests)

  • Test how components work together
  • More realistic than unit tests
  • "Can users complete the login flow?"

🔺 End-to-End Tests (Top - Few tests)

  • Test complete user journeys
  • Slowest but most realistic
  • "Can users browse products, add to cart, and checkout?"

The Testing Mindset Don't test implementation details - test behavior from the user's perspective: ❌ "The state variable 'isOpen' is true" ✅ "The dropdown menu is visible"

❌ "The onClick handler is called" ✅ "The form is submitted when clicking the button"

1// 🎯 YOUR FIRST TEST
2
3// Simple component to test
4function Greeting({ name }) {
5 return (
6 <div>
7 <h1>Hello, {name || 'Stranger'}!</h1>
8 <p>Welcome to our app</p>
9 </div>
10 );
11}
12
13// Test file
14import { render, screen } from '@testing-library/react';
15import Greeting from './Greeting';
16
17describe('Greeting Component', () => {
18 test('displays greeting with provided name', () => {
19 render(<Greeting name="Alice" />);
20 expect(screen.getByText('Hello, Alice!')).toBeInTheDocument();
21 });
22
23 test('displays default greeting when no name provided', () => {
24 render(<Greeting />);
25 expect(screen.getByText(/hello, stranger/i)).toBeInTheDocument();
26 });
27});
28
29// 🔍 QUERY METHODS - How to Find Elements
30
31function QueryExamples() {
32 return (
33 <div>
34 <h1>My App</h1>
35 <label htmlFor="username">Username:</label>
36 <input id="username" type="text" placeholder="Enter username" />
37 <button>Submit</button>
38 <img src="logo.png" alt="Company Logo" />
39 <div data-testid="custom-element">Custom Content</div>
40 </div>
41 );
42}
43
44// Demonstrating different query methods
45test('different ways to query elements', () => {
46 render(<QueryExamples />);
47
48 // Preferred: By Role (accessible)
49 expect(screen.getByRole('heading', { name: 'My App' })).toBeInTheDocument();
50 expect(screen.getByRole('button', { name: 'Submit' })).toBeInTheDocument();
51
52 // For form elements
53 expect(screen.getByLabelText('Username:')).toBeInTheDocument();
54 expect(screen.getByPlaceholderText('Enter username')).toBeInTheDocument();
55
56 // For text content
57 expect(screen.getByText('My App')).toBeInTheDocument();
58
59 // Last resort
60 expect(screen.getByTestId('custom-element')).toBeInTheDocument();
61});
62
63// 📊 QUERY PRIORITY (Best to Worst)
64/*
651. getByRole - Reflects how users and assistive tech see your app
662. getByLabelText - Good for form elements
673. getByPlaceholderText - If no label is present
684. getByText - For non-interactive elements
695. getByDisplayValue - Current value of form elements
706. getByAltText - For images
717. getByTitle - If element has title attribute
728. getByTestId - Last resort, not user-visible
73
74Each query has variants:
75- getBy... - Returns element or throws error
76- queryBy... - Returns element or null
77- findBy... - Returns promise (for async elements)
78- getAllBy... - Returns array of elements
79- queryAllBy... - Returns array or empty array
80- findAllBy... - Returns promise that resolves to array
81*/
82
83// 🎭 COMMON ASSERTIONS
84
85test('common assertion examples', () => {
86 render(
87 <div>
88 <button disabled>Click me</button>
89 <p style={{ color: 'red' }}>Error text</p>
90 <input type="email" required />
91 <a href="/home">Home</a>
92 </div>
93 );
94
95 // Existence
96 expect(screen.getByText('Error text')).toBeInTheDocument();
97 expect(screen.queryByText('Not here')).not.toBeInTheDocument();
98
99 // State
100 expect(screen.getByRole('button')).toBeDisabled();
101 expect(screen.getByRole('textbox')).toBeRequired();
102
103 // Attributes
104 expect(screen.getByRole('link')).toHaveAttribute('href', '/home');
105
106 // Styles
107 expect(screen.getByText('Error text')).toHaveStyle({ color: 'red' });
108});
109
110// 🧪 TEST LIFECYCLE HOOKS
111
112describe('Test lifecycle', () => {
113 // Setup before all tests
114 beforeAll(() => console.log('Suite setup'));
115 afterAll(() => console.log('Suite teardown'));
116
117 // Setup before each test
118 beforeEach(() => console.log('Test setup'));
119 afterEach(() => console.log('Test cleanup'));
120
121 test('runs with lifecycle hooks', () => {
122 expect(true).toBe(true);
123 });
124});

Testing Fundamentals: Setting Up Your Testing Environment

Testing ensures your React applications work correctly and helps prevent regressions when making changes. React applications typically use Jest as the test runner and React Testing Library for component testing.

Types of Tests:

  • Unit Tests: Test individual components in isolation
  • Integration Tests: Test how components work together
  • End-to-End Tests: Test complete user workflows
  • Snapshot Tests: Capture component output to detect unexpected changes

Testing Tools Overview

  1. Jest: The test runner that executes your tests

    • Provides test structure (describe, test, expect)
    • Mocking capabilities
    • Code coverage reports
    • Watch mode for development
  2. React Testing Library: Tests components from user perspective

    • Renders components
    • Provides queries to find elements
    • Simulates user interactions
    • Encourages accessible code
  3. User Event: Simulates real user interactions

    • More realistic than fireEvent
    • Handles browser behaviors properly

Testing Philosophy "The more your tests resemble the way your software is used, the more confidence they can give you." - React Testing Library Guiding Principle

1// 🚀 SETTING UP YOUR TESTING ENVIRONMENT
2
3// Step 1: Installation
4// npm install --save-dev @testing-library/react @testing-library/jest-dom @testing-library/user-event jest-environment-jsdom
5
6// Step 2: Configure Jest (jest.config.js)
7module.exports = {
8 // Use jsdom for DOM manipulation
9 testEnvironment: 'jsdom',
10
11 // Setup files to run after Jest is initialized
12 setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],
13
14 // Module path aliases
15 moduleNameMapper: {
16 '^@/(.*)$': '<rootDir>/src/$1',
17 '\.(css|less|scss|sass)$': 'identity-obj-proxy',
18 '\.(jpg|jpeg|png|gif|svg)$': '<rootDir>/__mocks__/fileMock.js'
19 },
20
21 // Coverage collection
22 collectCoverageFrom: [
23 'src/**/*.{js,jsx,ts,tsx}',
24 '!src/index.js',
25 '!src/reportWebVitals.js',
26 '!**/*.d.ts',
27 '!**/node_modules/**',
28 ],
29
30 // Transform files
31 transform: {
32 '^.+\.(js|jsx|ts|tsx)$': ['babel-jest', {
33 presets: [
34 ['@babel/preset-env', { targets: { node: 'current' } }],
35 '@babel/preset-react'
36 ]
37 }]
38 },
39
40 // Test match patterns
41 testMatch: [
42 '<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}',
43 '<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}'
44 ],
45
46 // Watch mode exclusions
47 watchPathIgnorePatterns: ['node_modules'],
48
49 // Module file extensions
50 moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx', 'json'],
51};
52
53// Step 3: Setup file (src/setupTests.js)
54import '@testing-library/jest-dom';
55
56// Add custom matchers
57import { expect } from '@jest/globals';
58
59// Custom matcher example
60expect.extend({
61 toBeWithinRange(received, floor, ceiling) {
62 const pass = received >= floor && received <= ceiling;
63 if (pass) {
64 return {
65 message: () => `expected ${received} not to be within range ${floor} - ${ceiling}`,
66 pass: true,
67 };
68 } else {
69 return {
70 message: () => `expected ${received} to be within range ${floor} - ${ceiling}`,
71 pass: false,
72 };
73 }
74 },
75});
76
77// Global test utilities
78global.localStorage = {
79 getItem: jest.fn(),
80 setItem: jest.fn(),
81 removeItem: jest.fn(),
82 clear: jest.fn(),
83};
84
85// Mock console methods to reduce noise in tests
86global.console = {
87 ...console,
88 error: jest.fn(),
89 warn: jest.fn(),
90};
91
92// Step 4: File mocks (__mocks__/fileMock.js)
93module.exports = 'test-file-stub';
94
95// Step 5: Style mock (__mocks__/styleMock.js)
96module.exports = {};
97
98// 📝 PACKAGE.JSON SCRIPTS
99
100{
101 "scripts": {
102 "test": "jest",
103 "test:watch": "jest --watch",
104 "test:coverage": "jest --coverage",
105 "test:debug": "node --inspect-brk ./node_modules/.bin/jest --runInBand",
106 "test:ci": "jest --ci --coverage --maxWorkers=2"
107 }
108}
109
110// 🎯 TYPESCRIPT CONFIGURATION (tsconfig.json)
111
112{
113 "compilerOptions": {
114 "target": "es5",
115 "lib": ["dom", "es2015"],
116 "jsx": "react",
117 "module": "commonjs",
118 "esModuleInterop": true,
119 "skipLibCheck": true,
120 "strict": true,
121 "types": ["jest", "@testing-library/jest-dom"]
122 },
123 "include": ["src"],
124 "exclude": ["node_modules"]
125}
126
127// 🔧 ESLINT CONFIGURATION FOR TESTS
128
129{
130 "overrides": [
131 {
132 "files": ["**/*.test.js", "**/*.test.jsx", "**/*.spec.js"],
133 "env": {
134 "jest": true
135 },
136 "rules": {
137 "no-unused-expressions": "off"
138 }
139 }
140 ]
141}
142
143// 🏗️ RECOMMENDED FOLDER STRUCTURE
144
145/*
146src/
147 components/
148 Button/
149 Button.js
150 Button.test.js
151 Button.stories.js (if using Storybook)
152 index.js
153
154 hooks/
155 useCounter/
156 useCounter.js
157 useCounter.test.js
158 index.js
159
160 utils/
161 formatters/
162 formatters.js
163 formatters.test.js
164
165 __tests__/ // For integration tests
166 integration/
167 userFlow.test.js
168
169 __mocks__/ // For manual mocks
170 axios.js
171
172 setupTests.js // Global test setup
173*/
174
175// 🎨 TESTING UTILITIES FILE (src/test-utils.js)
176
177import React from 'react';
178import { render } from '@testing-library/react';
179import { BrowserRouter } from 'react-router-dom';
180import { Provider } from 'react-redux';
181import { ThemeProvider } from './contexts/ThemeContext';
182
183// Custom render function that includes providers
184export function renderWithProviders(
185 ui,
186 {
187 preloadedState = {},
188 store = configureStore({ reducer: rootReducer, preloadedState }),
189 ...renderOptions
190 } = {}
191) {
192 function Wrapper({ children }) {
193 return (
194 <Provider store={store}>
195 <BrowserRouter>
196 <ThemeProvider>
197 {children}
198 </ThemeProvider>
199 </BrowserRouter>
200 </Provider>
201 );
202 }
203
204 return { store, ...render(ui, { wrapper: Wrapper, ...renderOptions }) };
205}
206
207// Re-export everything
208export * from '@testing-library/react';
209export { renderWithProviders as render };
210
211// 🚦 RUNNING TESTS
212
213// Run all tests
214// npm test
215
216// Run tests in watch mode (reruns on file changes)
217// npm run test:watch
218
219// Run tests with coverage report
220// npm run test:coverage
221
222// Run a specific test file
223// npm test Button.test.js
224
225// Run tests matching a pattern
226// npm test -- --testNamePattern="should render"
227
228// Update snapshots
229// npm test -- -u
230
231// Debug tests
232// npm run test:debug

Component Testing with React Testing Library

React Testing Library focuses on testing components from a user's perspective. Instead of testing implementation details, it encourages testing the behavior users will experience.

Core Principles:

  1. Test user behavior, not implementation
  2. Find elements the way users do
  3. Avoid testing internal state
  4. Make tests maintainable

The Testing Flow:

  1. Arrange: Set up your component and its props
  2. Act: Simulate user interactions
  3. Assert: Check the expected outcome

User Event vs FireEvent

  • userEvent: Simulates real user interactions (recommended)
  • fireEvent: Low-level event firing (use sparingly)

Testing Best Practices:

  • Write descriptive test names
  • Test one thing per test
  • Use accessible queries
  • Don't test implementation details
  • Keep tests simple and readable
1// 🧪 COMPREHENSIVE COMPONENT TESTING EXAMPLES
2
3import React, { useState } from 'react';
4import { render, screen, fireEvent, waitFor, within } from '@testing-library/react';
5import userEvent from '@testing-library/user-event';
6
7// ===================================
8// EXAMPLE 1: Simple Button Component
9// ===================================
10
11function Button({ children, onClick, disabled, loading }) {
12 return (
13 <button
14 onClick={onClick}
15 disabled={disabled || loading}
16 >
17 {loading ? 'Loading...' : children}
18 </button>
19 );
20}
21
22describe('Button Component', () => {
23 test('renders and handles clicks', async () => {
24 const user = userEvent.setup();
25 const handleClick = jest.fn();
26
27 render(<Button onClick={handleClick}>Click me</Button>);
28
29 const button = screen.getByRole('button', { name: 'Click me' });
30 await user.click(button);
31
32 expect(handleClick).toHaveBeenCalledTimes(1);
33 });
34
35 test('disables when loading', () => {
36 render(<Button loading>Save</Button>);
37
38 const button = screen.getByRole('button');
39 expect(button).toHaveTextContent('Loading...');
40 expect(button).toBeDisabled();
41 });
42});
43
44// 📝 EXAMPLE 2: Testing a Form Component
45
46function ContactForm({ onSubmit }) {
47 const [formData, setFormData] = useState({ name: '', email: '' });
48 const [error, setError] = useState('');
49
50 const handleSubmit = (e) => {
51 e.preventDefault();
52 if (!formData.name || !formData.email) {
53 setError('All fields are required');
54 return;
55 }
56 onSubmit(formData);
57 };
58
59 const handleChange = (e) => {
60 const { name, value } = e.target;
61 setFormData(prev => ({ ...prev, [name]: value }));
62 setError('');
63 };
64
65 return (
66 <form onSubmit={handleSubmit}>
67 <input
68 name="name"
69 placeholder="Name"
70 value={formData.name}
71 onChange={handleChange}
72 />
73 <input
74 name="email"
75 type="email"
76 placeholder="Email"
77 value={formData.email}
78 onChange={handleChange}
79 />
80 {error && <span role="alert">{error}</span>}
81 <button type="submit">Submit</button>
82 </form>
83 );
84}
85
86describe('ContactForm', () => {
87 test('shows validation error', async () => {
88 const user = userEvent.setup();
89 render(<ContactForm onSubmit={jest.fn()} />);
90
91 await user.click(screen.getByRole('button', { name: 'Submit' }));
92
93 expect(screen.getByRole('alert')).toHaveTextContent('All fields are required');
94 });
95
96 test('submits with valid data', async () => {
97 const user = userEvent.setup();
98 const mockSubmit = jest.fn();
99 render(<ContactForm onSubmit={mockSubmit} />);
100
101 await user.type(screen.getByPlaceholderText('Name'), 'John');
102 await user.type(screen.getByPlaceholderText('Email'), 'john@example.com');
103 await user.click(screen.getByRole('button'));
104
105 expect(mockSubmit).toHaveBeenCalledWith({
106 name: 'John',
107 email: 'john@example.com'
108 });
109 });
110});
111
112// =====================================
113// EXAMPLE 3: Todo List Component
114// =====================================
115
116function TodoList() {
117 const [todos, setTodos] = useState([]);
118 const [input, setInput] = useState('');
119
120 const addTodo = () => {
121 if (input.trim()) {
122 setTodos([...todos, { id: Date.now(), text: input, done: false }]);
123 setInput('');
124 }
125 };
126
127 const toggleTodo = (id) => {
128 setTodos(todos.map(todo =>
129 todo.id === id ? { ...todo, done: !todo.done } : todo
130 ));
131 };
132
133 return (
134 <div>
135 <input
136 value={input}
137 onChange={(e) => setInput(e.target.value)}
138 placeholder="Add todo"
139 />
140 <button onClick={addTodo}>Add</button>
141
142 <ul>
143 {todos.map(todo => (
144 <li key={todo.id}>
145 <input
146 type="checkbox"
147 checked={todo.done}
148 onChange={() => toggleTodo(todo.id)}
149 />
150 <span style={{ textDecoration: todo.done ? 'line-through' : 'none' }}>
151 {todo.text}
152 </span>
153 </li>
154 ))}
155 </ul>
156 </div>
157 );
158}
159
160describe('TodoList', () => {
161 test('adds and toggles todos', async () => {
162 const user = userEvent.setup();
163 render(<TodoList />);
164
165 const input = screen.getByPlaceholderText('Add todo');
166 await user.type(input, 'Test item');
167 await user.click(screen.getByText('Add'));
168
169 expect(screen.getByText('Test item')).toBeInTheDocument();
170
171 const checkbox = screen.getByRole('checkbox');
172 await user.click(checkbox);
173
174 expect(screen.getByText('Test item')).toHaveStyle({ textDecoration: 'line-through' });
175 });
176});
177
178// 🧪 TESTING PATTERNS
179
180// Testing async updates
181test('testing async behavior', async () => {
182 function AsyncComponent() {
183 const [data, setData] = useState('Loading...');
184
185 useEffect(() => {
186 setTimeout(() => setData('Loaded!'), 100);
187 }, []);
188
189 return <div>{data}</div>;
190 }
191
192 render(<AsyncComponent />);
193
194 expect(screen.getByText('Loading...')).toBeInTheDocument();
195
196 await waitFor(() => {
197 expect(screen.getByText('Loaded!')).toBeInTheDocument();
198 });
199});

Testing Hooks and Context

Testing custom hooks and context providers requires special techniques. React Testing Library provides utilities like renderHook for testing hooks in isolation.

Testing Custom Hooks:

  • Use renderHook to test hooks outside of components
  • Test the hook's return values and updates
  • Verify side effects and cleanup

Testing Context:

  • Create wrapper components with providers
  • Test context value changes
  • Verify consumer components update correctly

Best Practices:

  1. Test hooks behavior, not implementation
  2. Include edge cases and error states
  3. Test cleanup functions
  4. Verify memoization works correctly
1// 🪝 TESTING CUSTOM HOOKS
2
3import { renderHook, act } from '@testing-library/react';
4import { useState, useEffect, useContext, createContext } from 'react';
5
6// === EXAMPLE 1: Testing a Simple Counter Hook ===
7
8function useCounter(initialValue = 0) {
9 const [count, setCount] = useState(initialValue);
10
11 const increment = () => setCount(prev => prev + 1);
12 const decrement = () => setCount(prev => prev - 1);
13 const reset = () => setCount(initialValue);
14
15 return { count, increment, decrement, reset };
16}
17
18// Test the hook
19describe('useCounter', () => {
20 test('initializes and updates count', () => {
21 const { result } = renderHook(() => useCounter(5));
22
23 // Check initial value
24 expect(result.current.count).toBe(5);
25
26 // Test increment
27 act(() => {
28 result.current.increment();
29 });
30 expect(result.current.count).toBe(6);
31
32 // Test reset
33 act(() => {
34 result.current.reset();
35 });
36 expect(result.current.count).toBe(5);
37 });
38});
39
40// === EXAMPLE 2: Testing an Async Hook ===
41
42function useFetch(url) {
43 const [data, setData] = useState(null);
44 const [loading, setLoading] = useState(true);
45 const [error, setError] = useState(null);
46
47 useEffect(() => {
48 let cancelled = false;
49
50 async function fetchData() {
51 try {
52 setLoading(true);
53 const response = await fetch(url);
54 if (!response.ok) throw new Error('Failed to fetch');
55
56 const data = await response.json();
57 if (!cancelled) {
58 setData(data);
59 setError(null);
60 }
61 } catch (err) {
62 if (!cancelled) {
63 setError(err.message);
64 setData(null);
65 }
66 } finally {
67 if (!cancelled) {
68 setLoading(false);
69 }
70 }
71 }
72
73 fetchData();
74
75 return () => {
76 cancelled = true;
77 };
78 }, [url]);
79
80 return { data, loading, error };
81}
82
83// Test async hook
84describe('useFetch', () => {
85 beforeEach(() => {
86 global.fetch = jest.fn();
87 });
88
89 test('fetches data successfully', async () => {
90 const mockData = { id: 1, name: 'Test' };
91 fetch.mockResolvedValueOnce({
92 ok: true,
93 json: async () => mockData,
94 });
95
96 const { result } = renderHook(() =>
97 useFetch('https://api.example.com/data')
98 );
99
100 // Initially loading
101 expect(result.current.loading).toBe(true);
102 expect(result.current.data).toBe(null);
103
104 // Wait for fetch to complete
105 await waitFor(() => {
106 expect(result.current.loading).toBe(false);
107 });
108
109 expect(result.current.data).toEqual(mockData);
110 expect(result.current.error).toBe(null);
111 });
112
113 test('handles fetch error', async () => {
114 fetch.mockRejectedValueOnce(new Error('Network error'));
115
116 const { result } = renderHook(() =>
117 useFetch('https://api.example.com/data')
118 );
119
120 await waitFor(() => {
121 expect(result.current.loading).toBe(false);
122 });
123
124 expect(result.current.error).toBe('Network error');
125 expect(result.current.data).toBe(null);
126 });
127});
128
129// === EXAMPLE 3: Testing Context ===
130
131// Theme Context
132const ThemeContext = createContext();
133
134function ThemeProvider({ children }) {
135 const [theme, setTheme] = useState('light');
136
137 const toggleTheme = () => {
138 setTheme(prev => prev === 'light' ? 'dark' : 'light');
139 };
140
141 return (
142 <ThemeContext.Provider value={{ theme, toggleTheme }}>
143 {children}
144 </ThemeContext.Provider>
145 );
146}
147
148function useTheme() {
149 const context = useContext(ThemeContext);
150 if (!context) {
151 throw new Error('useTheme must be used within ThemeProvider');
152 }
153 return context;
154}
155
156// Test Context
157describe('ThemeContext', () => {
158 test('provides theme value and toggle function', () => {
159 function TestComponent() {
160 const { theme, toggleTheme } = useTheme();
161 return (
162 <div>
163 <p>Theme: {theme}</p>
164 <button onClick={toggleTheme}>Toggle</button>
165 </div>
166 );
167 }
168
169 render(
170 <ThemeProvider>
171 <TestComponent />
172 </ThemeProvider>
173 );
174
175 // Check initial theme
176 expect(screen.getByText('Theme: light')).toBeInTheDocument();
177
178 // Toggle theme
179 fireEvent.click(screen.getByText('Toggle'));
180 expect(screen.getByText('Theme: dark')).toBeInTheDocument();
181 });
182
183 test('throws error when used outside provider', () => {
184 // Test component that uses hook incorrectly
185 function BadComponent() {
186 useTheme(); // This should throw
187 return null;
188 }
189
190 // Suppress console.error for this test
191 const spy = jest.spyOn(console, 'error').mockImplementation();
192
193 expect(() => {
194 render(<BadComponent />);
195 }).toThrow('useTheme must be used within ThemeProvider');
196
197 spy.mockRestore();
198 });
199});
200
201// === EXAMPLE 4: Testing Hook with localStorage ===
202
203function useLocalStorage(key, initialValue) {
204 const [storedValue, setStoredValue] = useState(() => {
205 try {
206 const item = window.localStorage.getItem(key);
207 return item ? JSON.parse(item) : initialValue;
208 } catch (error) {
209 console.error(error);
210 return initialValue;
211 }
212 });
213
214 const setValue = (value) => {
215 try {
216 setStoredValue(value);
217 window.localStorage.setItem(key, JSON.stringify(value));
218 } catch (error) {
219 console.error(error);
220 }
221 };
222
223 return [storedValue, setValue];
224}
225
226// Test localStorage hook
227describe('useLocalStorage', () => {
228 beforeEach(() => {
229 localStorage.clear();
230 });
231
232 test('initializes with value from localStorage', () => {
233 localStorage.setItem('testKey', JSON.stringify('stored value'));
234
235 const { result } = renderHook(() =>
236 useLocalStorage('testKey', 'default')
237 );
238
239 expect(result.current[0]).toBe('stored value');
240 });
241
242 test('updates localStorage when value changes', () => {
243 const { result } = renderHook(() =>
244 useLocalStorage('testKey', 'initial')
245 );
246
247 act(() => {
248 result.current[1]('new value');
249 });
250
251 expect(result.current[0]).toBe('new value');
252 expect(localStorage.getItem('testKey')).toBe('"new value"');
253 });
254});
255
256// === Testing Tips ===
257/*
2581. Use renderHook for testing hooks in isolation
2592. Wrap state updates in act() to avoid warnings
2603. Use waitFor for async operations
2614. Mock external dependencies (fetch, localStorage, etc.)
2625. Test error cases and edge conditions
2636. Always clean up after tests (clear mocks, localStorage, etc.)
264*/

Mocking and Integration Testing

Mocking external dependencies and testing component integration ensures your tests are reliable and fast. This includes mocking API calls, external libraries, and complex dependencies.

When to Mock:

  • External API calls
  • Browser APIs (localStorage, fetch)
  • Third-party libraries
  • Timers and dates
  • Complex dependencies

Types of Mocks:

  1. Function mocks: jest.fn()
  2. Module mocks: jest.mock()
  3. Timer mocks: jest.useFakeTimers()
  4. Manual mocks: mocks folder

Integration Testing:

  • Test multiple components together
  • Verify data flow between components
  • Test complete user workflows
  • Focus on critical paths
1// 🎭 MOCKING AND INTEGRATION TESTING
2
3import React, { useState, useEffect } from 'react';
4import { render, screen, waitFor } from '@testing-library/react';
5import userEvent from '@testing-library/user-event';
6
7// === EXAMPLE 1: Mocking API Calls ===
8
9// Simple API module
10const api = {
11 fetchTodos: async () => {
12 const response = await fetch('/api/todos');
13 if (!response.ok) throw new Error('Failed to fetch');
14 return response.json();
15 },
16
17 createTodo: async (text) => {
18 const response = await fetch('/api/todos', {
19 method: 'POST',
20 headers: { 'Content-Type': 'application/json' },
21 body: JSON.stringify({ text, completed: false }),
22 });
23 if (!response.ok) throw new Error('Failed to create');
24 return response.json();
25 }
26};
27
28// TodoList component
29function TodoList() {
30 const [todos, setTodos] = useState([]);
31 const [loading, setLoading] = useState(true);
32 const [error, setError] = useState(null);
33 const [input, setInput] = useState('');
34
35 useEffect(() => {
36 loadTodos();
37 }, []);
38
39 const loadTodos = async () => {
40 try {
41 setLoading(true);
42 const data = await api.fetchTodos();
43 setTodos(data);
44 } catch (err) {
45 setError(err.message);
46 } finally {
47 setLoading(false);
48 }
49 };
50
51 const addTodo = async (e) => {
52 e.preventDefault();
53 if (!input.trim()) return;
54
55 try {
56 const newTodo = await api.createTodo(input);
57 setTodos([...todos, newTodo]);
58 setInput('');
59 } catch (err) {
60 setError(err.message);
61 }
62 };
63
64 if (loading) return <div>Loading...</div>;
65 if (error) return <div role="alert">Error: {error}</div>;
66
67 return (
68 <div>
69 <form onSubmit={addTodo}>
70 <input
71 value={input}
72 onChange={(e) => setInput(e.target.value)}
73 placeholder="Add todo"
74 />
75 <button type="submit">Add</button>
76 </form>
77
78 <ul>
79 {todos.map(todo => (
80 <li key={todo.id}>{todo.text}</li>
81 ))}
82 </ul>
83 </div>
84 );
85}
86
87// Test with mocked API
88jest.mock('./api');
89
90describe('TodoList', () => {
91 beforeEach(() => {
92 jest.clearAllMocks();
93 });
94
95 test('loads and displays todos', async () => {
96 const mockTodos = [
97 { id: 1, text: 'Test todo 1' },
98 { id: 2, text: 'Test todo 2' }
99 ];
100
101 api.fetchTodos.mockResolvedValueOnce(mockTodos);
102
103 render(<TodoList />);
104
105 expect(screen.getByText('Loading...')).toBeInTheDocument();
106
107 await waitFor(() => {
108 expect(screen.getByText('Test todo 1')).toBeInTheDocument();
109 expect(screen.getByText('Test todo 2')).toBeInTheDocument();
110 });
111 });
112
113 test('handles API error', async () => {
114 api.fetchTodos.mockRejectedValueOnce(new Error('Network error'));
115
116 render(<TodoList />);
117
118 await waitFor(() => {
119 expect(screen.getByRole('alert')).toHaveTextContent('Error: Network error');
120 });
121 });
122
123 test('adds new todo', async () => {
124 const user = userEvent.setup();
125 api.fetchTodos.mockResolvedValueOnce([]);
126 api.createTodo.mockResolvedValueOnce({ id: 1, text: 'New todo' });
127
128 render(<TodoList />);
129
130 await waitFor(() => {
131 expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
132 });
133
134 await user.type(screen.getByPlaceholderText('Add todo'), 'New todo');
135 await user.click(screen.getByText('Add'));
136
137 await waitFor(() => {
138 expect(screen.getByText('New todo')).toBeInTheDocument();
139 });
140
141 expect(api.createTodo).toHaveBeenCalledWith('New todo');
142 });
143});
144
145// === EXAMPLE 2: Mocking Timers ===
146
147function Notification({ message, duration = 3000, onClose }) {
148 const [visible, setVisible] = useState(true);
149
150 useEffect(() => {
151 const timer = setTimeout(() => {
152 setVisible(false);
153 onClose?.();
154 }, duration);
155
156 return () => clearTimeout(timer);
157 }, [duration, onClose]);
158
159 if (!visible) return null;
160
161 return (
162 <div role="alert">
163 {message}
164 <button onClick={() => {
165 setVisible(false);
166 onClose?.();
167 }}>
168 Close
169 </button>
170 </div>
171 );
172}
173
174// Test with fake timers
175describe('Notification', () => {
176 beforeEach(() => {
177 jest.useFakeTimers();
178 });
179
180 afterEach(() => {
181 jest.useRealTimers();
182 });
183
184 test('auto-closes after duration', () => {
185 const onClose = jest.fn();
186 render(
187 <Notification
188 message="Test notification"
189 duration={5000}
190 onClose={onClose}
191 />
192 );
193
194 expect(screen.getByText('Test notification')).toBeInTheDocument();
195
196 // Fast-forward time
197 act(() => {
198 jest.advanceTimersByTime(5000);
199 });
200
201 expect(screen.queryByText('Test notification')).not.toBeInTheDocument();
202 expect(onClose).toHaveBeenCalledTimes(1);
203 });
204
205 test('can be closed manually', async () => {
206 const user = userEvent.setup({ delay: null });
207 const onClose = jest.fn();
208
209 render(
210 <Notification
211 message="Test notification"
212 onClose={onClose}
213 />
214 );
215
216 await user.click(screen.getByText('Close'));
217
218 expect(screen.queryByText('Test notification')).not.toBeInTheDocument();
219 expect(onClose).toHaveBeenCalledTimes(1);
220 });
221});
222
223// === EXAMPLE 3: Mocking localStorage ===
224
225function useSettings() {
226 const [settings, setSettings] = useState(() => {
227 const saved = localStorage.getItem('settings');
228 return saved ? JSON.parse(saved) : { theme: 'light', fontSize: 16 };
229 });
230
231 const updateSettings = (newSettings) => {
232 const updated = { ...settings, ...newSettings };
233 setSettings(updated);
234 localStorage.setItem('settings', JSON.stringify(updated));
235 };
236
237 return [settings, updateSettings];
238}
239
240// Test localStorage hook
241describe('useSettings', () => {
242 const localStorageMock = {
243 getItem: jest.fn(),
244 setItem: jest.fn(),
245 clear: jest.fn()
246 };
247
248 beforeEach(() => {
249 Object.defineProperty(window, 'localStorage', {
250 value: localStorageMock,
251 writable: true
252 });
253 });
254
255 test('loads settings from localStorage', () => {
256 const savedSettings = { theme: 'dark', fontSize: 18 };
257 localStorageMock.getItem.mockReturnValue(JSON.stringify(savedSettings));
258
259 const { result } = renderHook(() => useSettings());
260
261 expect(localStorageMock.getItem).toHaveBeenCalledWith('settings');
262 expect(result.current[0]).toEqual(savedSettings);
263 });
264
265 test('saves settings to localStorage', () => {
266 localStorageMock.getItem.mockReturnValue(null);
267
268 const { result } = renderHook(() => useSettings());
269
270 act(() => {
271 result.current[1]({ theme: 'dark' });
272 });
273
274 expect(localStorageMock.setItem).toHaveBeenCalledWith(
275 'settings',
276 JSON.stringify({ theme: 'dark', fontSize: 16 })
277 );
278 });
279});
280
281// === EXAMPLE 4: Integration Testing ===
282
283// Mini shopping cart app
284function ShoppingCart() {
285 const [items, setItems] = useState([]);
286 const [total, setTotal] = useState(0);
287
288 const addItem = (product) => {
289 setItems([...items, product]);
290 setTotal(total + product.price);
291 };
292
293 const removeItem = (index) => {
294 const item = items[index];
295 setItems(items.filter((_, i) => i !== index));
296 setTotal(total - item.price);
297 };
298
299 return (
300 <div>
301 <ProductList onAddToCart={addItem} />
302 <Cart
303 items={items}
304 total={total}
305 onRemove={removeItem}
306 />
307 </div>
308 );
309}
310
311function ProductList({ onAddToCart }) {
312 const products = [
313 { id: 1, name: 'Laptop', price: 999 },
314 { id: 2, name: 'Mouse', price: 29 }
315 ];
316
317 return (
318 <div>
319 <h2>Products</h2>
320 {products.map(product => (
321 <div key={product.id}>
322 <span>{product.name} - ${product.price}</span>
323 <button onClick={() => onAddToCart(product)}>
324 Add to Cart
325 </button>
326 </div>
327 ))}
328 </div>
329 );
330}
331
332function Cart({ items, total, onRemove }) {
333 return (
334 <div>
335 <h2>Cart ({items.length} items)</h2>
336 {items.map((item, index) => (
337 <div key={index}>
338 <span>{item.name}</span>
339 <button onClick={() => onRemove(index)}>Remove</button>
340 </div>
341 ))}
342 <p>Total: ${total}</p>
343 </div>
344 );
345}
346
347// Integration test
348describe('ShoppingCart Integration', () => {
349 test('complete shopping flow', async () => {
350 const user = userEvent.setup();
351 render(<ShoppingCart />);
352
353 // Initial state
354 expect(screen.getByText('Cart (0 items)')).toBeInTheDocument();
355 expect(screen.getByText('Total: $0')).toBeInTheDocument();
356
357 // Add laptop to cart
358 const addLaptopBtn = screen.getAllByText('Add to Cart')[0];
359 await user.click(addLaptopBtn);
360
361 expect(screen.getByText('Cart (1 items)')).toBeInTheDocument();
362 expect(screen.getByText('Laptop')).toBeInTheDocument();
363 expect(screen.getByText('Total: $999')).toBeInTheDocument();
364
365 // Add mouse to cart
366 const addMouseBtn = screen.getAllByText('Add to Cart')[1];
367 await user.click(addMouseBtn);
368
369 expect(screen.getByText('Cart (2 items)')).toBeInTheDocument();
370 expect(screen.getByText('Total: $1028')).toBeInTheDocument();
371
372 // Remove laptop
373 const removeButtons = screen.getAllByText('Remove');
374 await user.click(removeButtons[0]);
375
376 expect(screen.getByText('Cart (1 items)')).toBeInTheDocument();
377 expect(screen.queryByText('Laptop')).not.toBeInTheDocument();
378 expect(screen.getByText('Total: $29')).toBeInTheDocument();
379 });
380});
381
382// === Testing Best Practices ===
383/*
3841. Mock external dependencies (APIs, timers, storage)
3852. Test the integration between components
3863. Focus on user behavior, not implementation
3874. Keep mocks simple and close to reality
3885. Clean up after each test
3896. Use appropriate assertions
390*/