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?
- Catch Bugs Early: Find problems before users do
- Confidence to Change: Refactor without fear of breaking things
- Documentation: Tests show how components should be used
- Save Time: Automated tests are faster than manual testing
- 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 TEST23// Simple component to test4function Greeting({ name }) {5 return (6 <div>7 <h1>Hello, {name || 'Stranger'}!</h1>8 <p>Welcome to our app</p>9 </div>10 );11}1213// Test file14import { render, screen } from '@testing-library/react';15import Greeting from './Greeting';1617describe('Greeting Component', () => {18 test('displays greeting with provided name', () => {19 render(<Greeting name="Alice" />);20 expect(screen.getByText('Hello, Alice!')).toBeInTheDocument();21 });2223 test('displays default greeting when no name provided', () => {24 render(<Greeting />);25 expect(screen.getByText(/hello, stranger/i)).toBeInTheDocument();26 });27});2829// 🔍 QUERY METHODS - How to Find Elements3031function 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}4344// Demonstrating different query methods45test('different ways to query elements', () => {46 render(<QueryExamples />);4748 // Preferred: By Role (accessible)49 expect(screen.getByRole('heading', { name: 'My App' })).toBeInTheDocument();50 expect(screen.getByRole('button', { name: 'Submit' })).toBeInTheDocument();5152 // For form elements53 expect(screen.getByLabelText('Username:')).toBeInTheDocument();54 expect(screen.getByPlaceholderText('Enter username')).toBeInTheDocument();5556 // For text content57 expect(screen.getByText('My App')).toBeInTheDocument();5859 // Last resort60 expect(screen.getByTestId('custom-element')).toBeInTheDocument();61});6263// 📊 QUERY PRIORITY (Best to Worst)64/*651. getByRole - Reflects how users and assistive tech see your app662. getByLabelText - Good for form elements673. getByPlaceholderText - If no label is present684. getByText - For non-interactive elements695. getByDisplayValue - Current value of form elements706. getByAltText - For images717. getByTitle - If element has title attribute728. getByTestId - Last resort, not user-visible7374Each query has variants:75- getBy... - Returns element or throws error76- queryBy... - Returns element or null77- findBy... - Returns promise (for async elements)78- getAllBy... - Returns array of elements79- queryAllBy... - Returns array or empty array80- findAllBy... - Returns promise that resolves to array81*/8283// 🎭 COMMON ASSERTIONS8485test('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 );9495 // Existence96 expect(screen.getByText('Error text')).toBeInTheDocument();97 expect(screen.queryByText('Not here')).not.toBeInTheDocument();9899 // State100 expect(screen.getByRole('button')).toBeDisabled();101 expect(screen.getByRole('textbox')).toBeRequired();102103 // Attributes104 expect(screen.getByRole('link')).toHaveAttribute('href', '/home');105106 // Styles107 expect(screen.getByText('Error text')).toHaveStyle({ color: 'red' });108});109110// 🧪 TEST LIFECYCLE HOOKS111112describe('Test lifecycle', () => {113 // Setup before all tests114 beforeAll(() => console.log('Suite setup'));115 afterAll(() => console.log('Suite teardown'));116117 // Setup before each test118 beforeEach(() => console.log('Test setup'));119 afterEach(() => console.log('Test cleanup'));120121 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
-
Jest: The test runner that executes your tests
- Provides test structure (describe, test, expect)
- Mocking capabilities
- Code coverage reports
- Watch mode for development
-
React Testing Library: Tests components from user perspective
- Renders components
- Provides queries to find elements
- Simulates user interactions
- Encourages accessible code
-
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 ENVIRONMENT23// Step 1: Installation4// npm install --save-dev @testing-library/react @testing-library/jest-dom @testing-library/user-event jest-environment-jsdom56// Step 2: Configure Jest (jest.config.js)7module.exports = {8 // Use jsdom for DOM manipulation9 testEnvironment: 'jsdom',1011 // Setup files to run after Jest is initialized12 setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],1314 // Module path aliases15 moduleNameMapper: {16 '^@/(.*)$': '<rootDir>/src/$1',17 '\.(css|less|scss|sass)$': 'identity-obj-proxy',18 '\.(jpg|jpeg|png|gif|svg)$': '<rootDir>/__mocks__/fileMock.js'19 },2021 // Coverage collection22 collectCoverageFrom: [23 'src/**/*.{js,jsx,ts,tsx}',24 '!src/index.js',25 '!src/reportWebVitals.js',26 '!**/*.d.ts',27 '!**/node_modules/**',28 ],2930 // Transform files31 transform: {32 '^.+\.(js|jsx|ts|tsx)$': ['babel-jest', {33 presets: [34 ['@babel/preset-env', { targets: { node: 'current' } }],35 '@babel/preset-react'36 ]37 }]38 },3940 // Test match patterns41 testMatch: [42 '<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}',43 '<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}'44 ],4546 // Watch mode exclusions47 watchPathIgnorePatterns: ['node_modules'],4849 // Module file extensions50 moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx', 'json'],51};5253// Step 3: Setup file (src/setupTests.js)54import '@testing-library/jest-dom';5556// Add custom matchers57import { expect } from '@jest/globals';5859// Custom matcher example60expect.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});7677// Global test utilities78global.localStorage = {79 getItem: jest.fn(),80 setItem: jest.fn(),81 removeItem: jest.fn(),82 clear: jest.fn(),83};8485// Mock console methods to reduce noise in tests86global.console = {87 ...console,88 error: jest.fn(),89 warn: jest.fn(),90};9192// Step 4: File mocks (__mocks__/fileMock.js)93module.exports = 'test-file-stub';9495// Step 5: Style mock (__mocks__/styleMock.js)96module.exports = {};9798// 📝 PACKAGE.JSON SCRIPTS99100{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}109110// 🎯 TYPESCRIPT CONFIGURATION (tsconfig.json)111112{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}126127// 🔧 ESLINT CONFIGURATION FOR TESTS128129{130 "overrides": [131 {132 "files": ["**/*.test.js", "**/*.test.jsx", "**/*.spec.js"],133 "env": {134 "jest": true135 },136 "rules": {137 "no-unused-expressions": "off"138 }139 }140 ]141}142143// 🏗️ RECOMMENDED FOLDER STRUCTURE144145/*146src/147 components/148 Button/149 Button.js150 Button.test.js151 Button.stories.js (if using Storybook)152 index.js153154 hooks/155 useCounter/156 useCounter.js157 useCounter.test.js158 index.js159160 utils/161 formatters/162 formatters.js163 formatters.test.js164165 __tests__/ // For integration tests166 integration/167 userFlow.test.js168169 __mocks__/ // For manual mocks170 axios.js171172 setupTests.js // Global test setup173*/174175// 🎨 TESTING UTILITIES FILE (src/test-utils.js)176177import 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';182183// Custom render function that includes providers184export function renderWithProviders(185 ui,186 {187 preloadedState = {},188 store = configureStore({ reducer: rootReducer, preloadedState }),189 ...renderOptions190 } = {}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 }203204 return { store, ...render(ui, { wrapper: Wrapper, ...renderOptions }) };205}206207// Re-export everything208export * from '@testing-library/react';209export { renderWithProviders as render };210211// 🚦 RUNNING TESTS212213// Run all tests214// npm test215216// Run tests in watch mode (reruns on file changes)217// npm run test:watch218219// Run tests with coverage report220// npm run test:coverage221222// Run a specific test file223// npm test Button.test.js224225// Run tests matching a pattern226// npm test -- --testNamePattern="should render"227228// Update snapshots229// npm test -- -u230231// Debug tests232// 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:
- Test user behavior, not implementation
- Find elements the way users do
- Avoid testing internal state
- Make tests maintainable
The Testing Flow:
- Arrange: Set up your component and its props
- Act: Simulate user interactions
- 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 EXAMPLES23import React, { useState } from 'react';4import { render, screen, fireEvent, waitFor, within } from '@testing-library/react';5import userEvent from '@testing-library/user-event';67// ===================================8// EXAMPLE 1: Simple Button Component9// ===================================1011function Button({ children, onClick, disabled, loading }) {12 return (13 <button14 onClick={onClick}15 disabled={disabled || loading}16 >17 {loading ? 'Loading...' : children}18 </button>19 );20}2122describe('Button Component', () => {23 test('renders and handles clicks', async () => {24 const user = userEvent.setup();25 const handleClick = jest.fn();2627 render(<Button onClick={handleClick}>Click me</Button>);2829 const button = screen.getByRole('button', { name: 'Click me' });30 await user.click(button);3132 expect(handleClick).toHaveBeenCalledTimes(1);33 });3435 test('disables when loading', () => {36 render(<Button loading>Save</Button>);3738 const button = screen.getByRole('button');39 expect(button).toHaveTextContent('Loading...');40 expect(button).toBeDisabled();41 });42});4344// 📝 EXAMPLE 2: Testing a Form Component4546function ContactForm({ onSubmit }) {47 const [formData, setFormData] = useState({ name: '', email: '' });48 const [error, setError] = useState('');4950 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 };5859 const handleChange = (e) => {60 const { name, value } = e.target;61 setFormData(prev => ({ ...prev, [name]: value }));62 setError('');63 };6465 return (66 <form onSubmit={handleSubmit}>67 <input68 name="name"69 placeholder="Name"70 value={formData.name}71 onChange={handleChange}72 />73 <input74 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}8586describe('ContactForm', () => {87 test('shows validation error', async () => {88 const user = userEvent.setup();89 render(<ContactForm onSubmit={jest.fn()} />);9091 await user.click(screen.getByRole('button', { name: 'Submit' }));9293 expect(screen.getByRole('alert')).toHaveTextContent('All fields are required');94 });9596 test('submits with valid data', async () => {97 const user = userEvent.setup();98 const mockSubmit = jest.fn();99 render(<ContactForm onSubmit={mockSubmit} />);100101 await user.type(screen.getByPlaceholderText('Name'), 'John');102 await user.type(screen.getByPlaceholderText('Email'), 'john@example.com');103 await user.click(screen.getByRole('button'));104105 expect(mockSubmit).toHaveBeenCalledWith({106 name: 'John',107 email: 'john@example.com'108 });109 });110});111112// =====================================113// EXAMPLE 3: Todo List Component114// =====================================115116function TodoList() {117 const [todos, setTodos] = useState([]);118 const [input, setInput] = useState('');119120 const addTodo = () => {121 if (input.trim()) {122 setTodos([...todos, { id: Date.now(), text: input, done: false }]);123 setInput('');124 }125 };126127 const toggleTodo = (id) => {128 setTodos(todos.map(todo =>129 todo.id === id ? { ...todo, done: !todo.done } : todo130 ));131 };132133 return (134 <div>135 <input136 value={input}137 onChange={(e) => setInput(e.target.value)}138 placeholder="Add todo"139 />140 <button onClick={addTodo}>Add</button>141142 <ul>143 {todos.map(todo => (144 <li key={todo.id}>145 <input146 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}159160describe('TodoList', () => {161 test('adds and toggles todos', async () => {162 const user = userEvent.setup();163 render(<TodoList />);164165 const input = screen.getByPlaceholderText('Add todo');166 await user.type(input, 'Test item');167 await user.click(screen.getByText('Add'));168169 expect(screen.getByText('Test item')).toBeInTheDocument();170171 const checkbox = screen.getByRole('checkbox');172 await user.click(checkbox);173174 expect(screen.getByText('Test item')).toHaveStyle({ textDecoration: 'line-through' });175 });176});177178// 🧪 TESTING PATTERNS179180// Testing async updates181test('testing async behavior', async () => {182 function AsyncComponent() {183 const [data, setData] = useState('Loading...');184185 useEffect(() => {186 setTimeout(() => setData('Loaded!'), 100);187 }, []);188189 return <div>{data}</div>;190 }191192 render(<AsyncComponent />);193194 expect(screen.getByText('Loading...')).toBeInTheDocument();195196 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:
- Test hooks behavior, not implementation
- Include edge cases and error states
- Test cleanup functions
- Verify memoization works correctly
1// 🪝 TESTING CUSTOM HOOKS23import { renderHook, act } from '@testing-library/react';4import { useState, useEffect, useContext, createContext } from 'react';56// === EXAMPLE 1: Testing a Simple Counter Hook ===78function useCounter(initialValue = 0) {9 const [count, setCount] = useState(initialValue);1011 const increment = () => setCount(prev => prev + 1);12 const decrement = () => setCount(prev => prev - 1);13 const reset = () => setCount(initialValue);1415 return { count, increment, decrement, reset };16}1718// Test the hook19describe('useCounter', () => {20 test('initializes and updates count', () => {21 const { result } = renderHook(() => useCounter(5));2223 // Check initial value24 expect(result.current.count).toBe(5);2526 // Test increment27 act(() => {28 result.current.increment();29 });30 expect(result.current.count).toBe(6);3132 // Test reset33 act(() => {34 result.current.reset();35 });36 expect(result.current.count).toBe(5);37 });38});3940// === EXAMPLE 2: Testing an Async Hook ===4142function useFetch(url) {43 const [data, setData] = useState(null);44 const [loading, setLoading] = useState(true);45 const [error, setError] = useState(null);4647 useEffect(() => {48 let cancelled = false;4950 async function fetchData() {51 try {52 setLoading(true);53 const response = await fetch(url);54 if (!response.ok) throw new Error('Failed to fetch');5556 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 }7273 fetchData();7475 return () => {76 cancelled = true;77 };78 }, [url]);7980 return { data, loading, error };81}8283// Test async hook84describe('useFetch', () => {85 beforeEach(() => {86 global.fetch = jest.fn();87 });8889 test('fetches data successfully', async () => {90 const mockData = { id: 1, name: 'Test' };91 fetch.mockResolvedValueOnce({92 ok: true,93 json: async () => mockData,94 });9596 const { result } = renderHook(() =>97 useFetch('https://api.example.com/data')98 );99100 // Initially loading101 expect(result.current.loading).toBe(true);102 expect(result.current.data).toBe(null);103104 // Wait for fetch to complete105 await waitFor(() => {106 expect(result.current.loading).toBe(false);107 });108109 expect(result.current.data).toEqual(mockData);110 expect(result.current.error).toBe(null);111 });112113 test('handles fetch error', async () => {114 fetch.mockRejectedValueOnce(new Error('Network error'));115116 const { result } = renderHook(() =>117 useFetch('https://api.example.com/data')118 );119120 await waitFor(() => {121 expect(result.current.loading).toBe(false);122 });123124 expect(result.current.error).toBe('Network error');125 expect(result.current.data).toBe(null);126 });127});128129// === EXAMPLE 3: Testing Context ===130131// Theme Context132const ThemeContext = createContext();133134function ThemeProvider({ children }) {135 const [theme, setTheme] = useState('light');136137 const toggleTheme = () => {138 setTheme(prev => prev === 'light' ? 'dark' : 'light');139 };140141 return (142 <ThemeContext.Provider value={{ theme, toggleTheme }}>143 {children}144 </ThemeContext.Provider>145 );146}147148function useTheme() {149 const context = useContext(ThemeContext);150 if (!context) {151 throw new Error('useTheme must be used within ThemeProvider');152 }153 return context;154}155156// Test Context157describe('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 }168169 render(170 <ThemeProvider>171 <TestComponent />172 </ThemeProvider>173 );174175 // Check initial theme176 expect(screen.getByText('Theme: light')).toBeInTheDocument();177178 // Toggle theme179 fireEvent.click(screen.getByText('Toggle'));180 expect(screen.getByText('Theme: dark')).toBeInTheDocument();181 });182183 test('throws error when used outside provider', () => {184 // Test component that uses hook incorrectly185 function BadComponent() {186 useTheme(); // This should throw187 return null;188 }189190 // Suppress console.error for this test191 const spy = jest.spyOn(console, 'error').mockImplementation();192193 expect(() => {194 render(<BadComponent />);195 }).toThrow('useTheme must be used within ThemeProvider');196197 spy.mockRestore();198 });199});200201// === EXAMPLE 4: Testing Hook with localStorage ===202203function 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 });213214 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 };222223 return [storedValue, setValue];224}225226// Test localStorage hook227describe('useLocalStorage', () => {228 beforeEach(() => {229 localStorage.clear();230 });231232 test('initializes with value from localStorage', () => {233 localStorage.setItem('testKey', JSON.stringify('stored value'));234235 const { result } = renderHook(() =>236 useLocalStorage('testKey', 'default')237 );238239 expect(result.current[0]).toBe('stored value');240 });241242 test('updates localStorage when value changes', () => {243 const { result } = renderHook(() =>244 useLocalStorage('testKey', 'initial')245 );246247 act(() => {248 result.current[1]('new value');249 });250251 expect(result.current[0]).toBe('new value');252 expect(localStorage.getItem('testKey')).toBe('"new value"');253 });254});255256// === Testing Tips ===257/*2581. Use renderHook for testing hooks in isolation2592. Wrap state updates in act() to avoid warnings2603. Use waitFor for async operations2614. Mock external dependencies (fetch, localStorage, etc.)2625. Test error cases and edge conditions2636. 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:
- Function mocks: jest.fn()
- Module mocks: jest.mock()
- Timer mocks: jest.useFakeTimers()
- 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 TESTING23import React, { useState, useEffect } from 'react';4import { render, screen, waitFor } from '@testing-library/react';5import userEvent from '@testing-library/user-event';67// === EXAMPLE 1: Mocking API Calls ===89// Simple API module10const 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 },1617 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};2728// TodoList component29function TodoList() {30 const [todos, setTodos] = useState([]);31 const [loading, setLoading] = useState(true);32 const [error, setError] = useState(null);33 const [input, setInput] = useState('');3435 useEffect(() => {36 loadTodos();37 }, []);3839 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 };5051 const addTodo = async (e) => {52 e.preventDefault();53 if (!input.trim()) return;5455 try {56 const newTodo = await api.createTodo(input);57 setTodos([...todos, newTodo]);58 setInput('');59 } catch (err) {60 setError(err.message);61 }62 };6364 if (loading) return <div>Loading...</div>;65 if (error) return <div role="alert">Error: {error}</div>;6667 return (68 <div>69 <form onSubmit={addTodo}>70 <input71 value={input}72 onChange={(e) => setInput(e.target.value)}73 placeholder="Add todo"74 />75 <button type="submit">Add</button>76 </form>7778 <ul>79 {todos.map(todo => (80 <li key={todo.id}>{todo.text}</li>81 ))}82 </ul>83 </div>84 );85}8687// Test with mocked API88jest.mock('./api');8990describe('TodoList', () => {91 beforeEach(() => {92 jest.clearAllMocks();93 });9495 test('loads and displays todos', async () => {96 const mockTodos = [97 { id: 1, text: 'Test todo 1' },98 { id: 2, text: 'Test todo 2' }99 ];100101 api.fetchTodos.mockResolvedValueOnce(mockTodos);102103 render(<TodoList />);104105 expect(screen.getByText('Loading...')).toBeInTheDocument();106107 await waitFor(() => {108 expect(screen.getByText('Test todo 1')).toBeInTheDocument();109 expect(screen.getByText('Test todo 2')).toBeInTheDocument();110 });111 });112113 test('handles API error', async () => {114 api.fetchTodos.mockRejectedValueOnce(new Error('Network error'));115116 render(<TodoList />);117118 await waitFor(() => {119 expect(screen.getByRole('alert')).toHaveTextContent('Error: Network error');120 });121 });122123 test('adds new todo', async () => {124 const user = userEvent.setup();125 api.fetchTodos.mockResolvedValueOnce([]);126 api.createTodo.mockResolvedValueOnce({ id: 1, text: 'New todo' });127128 render(<TodoList />);129130 await waitFor(() => {131 expect(screen.queryByText('Loading...')).not.toBeInTheDocument();132 });133134 await user.type(screen.getByPlaceholderText('Add todo'), 'New todo');135 await user.click(screen.getByText('Add'));136137 await waitFor(() => {138 expect(screen.getByText('New todo')).toBeInTheDocument();139 });140141 expect(api.createTodo).toHaveBeenCalledWith('New todo');142 });143});144145// === EXAMPLE 2: Mocking Timers ===146147function Notification({ message, duration = 3000, onClose }) {148 const [visible, setVisible] = useState(true);149150 useEffect(() => {151 const timer = setTimeout(() => {152 setVisible(false);153 onClose?.();154 }, duration);155156 return () => clearTimeout(timer);157 }, [duration, onClose]);158159 if (!visible) return null;160161 return (162 <div role="alert">163 {message}164 <button onClick={() => {165 setVisible(false);166 onClose?.();167 }}>168 Close169 </button>170 </div>171 );172}173174// Test with fake timers175describe('Notification', () => {176 beforeEach(() => {177 jest.useFakeTimers();178 });179180 afterEach(() => {181 jest.useRealTimers();182 });183184 test('auto-closes after duration', () => {185 const onClose = jest.fn();186 render(187 <Notification188 message="Test notification"189 duration={5000}190 onClose={onClose}191 />192 );193194 expect(screen.getByText('Test notification')).toBeInTheDocument();195196 // Fast-forward time197 act(() => {198 jest.advanceTimersByTime(5000);199 });200201 expect(screen.queryByText('Test notification')).not.toBeInTheDocument();202 expect(onClose).toHaveBeenCalledTimes(1);203 });204205 test('can be closed manually', async () => {206 const user = userEvent.setup({ delay: null });207 const onClose = jest.fn();208209 render(210 <Notification211 message="Test notification"212 onClose={onClose}213 />214 );215216 await user.click(screen.getByText('Close'));217218 expect(screen.queryByText('Test notification')).not.toBeInTheDocument();219 expect(onClose).toHaveBeenCalledTimes(1);220 });221});222223// === EXAMPLE 3: Mocking localStorage ===224225function useSettings() {226 const [settings, setSettings] = useState(() => {227 const saved = localStorage.getItem('settings');228 return saved ? JSON.parse(saved) : { theme: 'light', fontSize: 16 };229 });230231 const updateSettings = (newSettings) => {232 const updated = { ...settings, ...newSettings };233 setSettings(updated);234 localStorage.setItem('settings', JSON.stringify(updated));235 };236237 return [settings, updateSettings];238}239240// Test localStorage hook241describe('useSettings', () => {242 const localStorageMock = {243 getItem: jest.fn(),244 setItem: jest.fn(),245 clear: jest.fn()246 };247248 beforeEach(() => {249 Object.defineProperty(window, 'localStorage', {250 value: localStorageMock,251 writable: true252 });253 });254255 test('loads settings from localStorage', () => {256 const savedSettings = { theme: 'dark', fontSize: 18 };257 localStorageMock.getItem.mockReturnValue(JSON.stringify(savedSettings));258259 const { result } = renderHook(() => useSettings());260261 expect(localStorageMock.getItem).toHaveBeenCalledWith('settings');262 expect(result.current[0]).toEqual(savedSettings);263 });264265 test('saves settings to localStorage', () => {266 localStorageMock.getItem.mockReturnValue(null);267268 const { result } = renderHook(() => useSettings());269270 act(() => {271 result.current[1]({ theme: 'dark' });272 });273274 expect(localStorageMock.setItem).toHaveBeenCalledWith(275 'settings',276 JSON.stringify({ theme: 'dark', fontSize: 16 })277 );278 });279});280281// === EXAMPLE 4: Integration Testing ===282283// Mini shopping cart app284function ShoppingCart() {285 const [items, setItems] = useState([]);286 const [total, setTotal] = useState(0);287288 const addItem = (product) => {289 setItems([...items, product]);290 setTotal(total + product.price);291 };292293 const removeItem = (index) => {294 const item = items[index];295 setItems(items.filter((_, i) => i !== index));296 setTotal(total - item.price);297 };298299 return (300 <div>301 <ProductList onAddToCart={addItem} />302 <Cart303 items={items}304 total={total}305 onRemove={removeItem}306 />307 </div>308 );309}310311function ProductList({ onAddToCart }) {312 const products = [313 { id: 1, name: 'Laptop', price: 999 },314 { id: 2, name: 'Mouse', price: 29 }315 ];316317 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 Cart325 </button>326 </div>327 ))}328 </div>329 );330}331332function 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}346347// Integration test348describe('ShoppingCart Integration', () => {349 test('complete shopping flow', async () => {350 const user = userEvent.setup();351 render(<ShoppingCart />);352353 // Initial state354 expect(screen.getByText('Cart (0 items)')).toBeInTheDocument();355 expect(screen.getByText('Total: $0')).toBeInTheDocument();356357 // Add laptop to cart358 const addLaptopBtn = screen.getAllByText('Add to Cart')[0];359 await user.click(addLaptopBtn);360361 expect(screen.getByText('Cart (1 items)')).toBeInTheDocument();362 expect(screen.getByText('Laptop')).toBeInTheDocument();363 expect(screen.getByText('Total: $999')).toBeInTheDocument();364365 // Add mouse to cart366 const addMouseBtn = screen.getAllByText('Add to Cart')[1];367 await user.click(addMouseBtn);368369 expect(screen.getByText('Cart (2 items)')).toBeInTheDocument();370 expect(screen.getByText('Total: $1028')).toBeInTheDocument();371372 // Remove laptop373 const removeButtons = screen.getAllByText('Remove');374 await user.click(removeButtons[0]);375376 expect(screen.getByText('Cart (1 items)')).toBeInTheDocument();377 expect(screen.queryByText('Laptop')).not.toBeInTheDocument();378 expect(screen.getByText('Total: $29')).toBeInTheDocument();379 });380});381382// === Testing Best Practices ===383/*3841. Mock external dependencies (APIs, timers, storage)3852. Test the integration between components3863. Focus on user behavior, not implementation3874. Keep mocks simple and close to reality3885. Clean up after each test3896. Use appropriate assertions390*/