Chapter 21:Testing in React
Testing in React — this is one of the most important chapters for writing reliable, production-ready code!
Testing is what separates hobby projects from professional applications. When you test your components and hooks properly, you:
- Catch bugs early
- Feel confident refactoring
- Make sure UI behaves correctly
- Sleep peacefully when deploying
We’ll focus on the modern, recommended way in 2026:
- React Testing Library (RTL) – the best tool for testing React components
- Jest – the test runner (comes with Create React App / Vite by default)
We’ll go very slowly and clearly, like I’m sitting next to you in Mumbai showing you live on the screen, with complete examples you can copy-paste right now.
1. Why React Testing Library (not Enzyme)?
| Tool | Philosophy | Recommendation in 2026 |
|---|---|---|
| React Testing Library | Test how users interact with your app, not implementation details | Official recommendation – use this! |
| Enzyme | Tests internal component state & structure | Deprecated – don’t use for new code |
RTL mantra (repeat after me!):
“The more your tests resemble the way your software is used, the more confidence they can give you.” — Kent C. Dodds
2. Setup (Vite + React + TypeScript)
If you used Vite + React, you already have Jest + Vitest support, but we’ll use Jest for this tutorial.
Install dependencies:
|
0 1 2 3 4 5 6 |
npm install --save-dev @testing-library/react @testing-library/jest-dom @testing-library/user-event jest ts-jest @types/jest |
Add to package.json:
|
0 1 2 3 4 5 6 7 8 |
"scripts": { "test": "jest" } |
Create jest.config.js (or jest.config.ts):
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 |
export default { preset: 'ts-jest', testEnvironment: 'jsdom', setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'], moduleNameMapper: { '\\.(css|less|scss|sass)$': 'identity-obj-proxy' } }; |
Create src/setupTests.ts:
|
0 1 2 3 4 5 6 |
import '@testing-library/jest-dom'; |
Now you’re ready!
3. Writing Your First Test – Simple Counter Component
Create src/components/Counter.tsx
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
import { useState } from 'react'; function Counter() { const [count, setCount] = useState(0); return ( <div className="p-8 text-center"> <h2 className="text-3xl font-bold mb-6">Counter</h2> <p className="text-4xl mb-6" data-testid="count"> {count} </p> <div className="flex justify-center gap-4"> <button onClick={() => setCount(c => c - 1)} className="px-6 py-3 bg-red-500 text-white rounded-lg hover:bg-red-600" data-testid="decrement" > - </button> <button onClick={() => setCount(c => c + 1)} className="px-6 py-3 bg-green-500 text-white rounded-lg hover:bg-green-600" data-testid="increment" > + </button> </div> </div> ); } export default Counter; |
Create test file: src/components/Counter.test.tsx
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
import { render, screen, fireEvent } from '@testing-library/react'; import Counter from './Counter'; describe('Counter Component', () => { it('renders initial count as 0', () => { render(<Counter />); const countElement = screen.getByTestId('count'); expect(countElement).toHaveTextContent('0'); }); it('increments count when + button is clicked', async () => { render(<Counter />); const incrementButton = screen.getByTestId('increment'); const countElement = screen.getByTestId('count'); fireEvent.click(incrementButton); expect(countElement).toHaveTextContent('1'); fireEvent.click(incrementButton); expect(countElement).toHaveTextContent('2'); }); it('decrements count when - button is clicked', () => { render(<Counter />); const decrementButton = screen.getByTestId('decrement'); const countElement = screen.getByTestId('count'); // First increment to 1 fireEvent.click(screen.getByTestId('increment')); expect(countElement).toHaveTextContent('1'); // Then decrement back to 0 fireEvent.click(decrementButton); expect(countElement).toHaveTextContent('0'); }); }); |
Run tests:
|
0 1 2 3 4 5 6 |
npm test |
4. Testing Hooks with @testing-library/react-hooks (or React 18+ built-in)
Modern way (React 18+): Use renderHook from @testing-library/react
Install:
|
0 1 2 3 4 5 6 |
npm install --save-dev @testing-library/react-hooks |
Example hook: src/hooks/useCounter.ts
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import { useState } from 'react'; export function useCounter(initial = 0) { const [count, setCount] = useState(initial); const increment = () => setCount(c => c + 1); const decrement = () => setCount(c => c - 1); const reset = () => setCount(initial); return { count, increment, decrement, reset }; } |
Test file: src/hooks/useCounter.test.ts
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
import { renderHook, act } from '@testing-library/react'; import { useCounter } from './useCounter'; describe('useCounter hook', () => { it('initializes with default value 0', () => { const { result } = renderHook(() => useCounter()); expect(result.current.count).toBe(0); }); it('initializes with custom initial value', () => { const { result } = renderHook(() => useCounter(10)); expect(result.current.count).toBe(10); }); it('increments count correctly', () => { const { result } = renderHook(() => useCounter()); act(() => { result.current.increment(); }); expect(result.current.count).toBe(1); act(() => { result.current.increment(); }); expect(result.current.count).toBe(2); }); it('decrements and resets correctly', () => { const { result } = renderHook(() => useCounter(5)); act(() => result.current.decrement()); expect(result.current.count).toBe(4); act(() => result.current.reset()); expect(result.current.count).toBe(5); }); }); |
5. Best Practices & Common Test Patterns
a. Query Methods (Priority Order)
| Method | Use when… |
|---|---|
| getByRole | Most preferred – closest to how users find elements |
| getByLabelText | For form inputs |
| getByPlaceholderText | When no label exists |
| getByText | For non-interactive text |
| getByTestId | Last resort – only when nothing else works |
b. Async Testing (Data Fetching)
|
0 1 2 3 4 5 6 7 8 9 10 11 |
it('displays users after loading', async () => { render(<UserList />); // Wait for loading to finish expect(await screen.findByText('Leanne Graham')).toBeInTheDocument(); }); |
c. Snapshot Testing (Optional)
|
0 1 2 3 4 5 6 7 8 9 |
it('matches snapshot', () => { const { asFragment } = render(<Counter />); expect(asFragment()).toMatchSnapshot(); }); |
Summary – Chapter 21 Key Takeaways
- Use React Testing Library – test behavior, not implementation
- Jest = test runner (easy setup with Vite/Next.js)
- render, screen, fireEvent, waitFor, findBy* → your main tools
- Test components → user interactions (click, type, etc.)
- Test hooks → renderHook + act
- Priority queries: getByRole > getByLabelText > getByText > getByTestId
- Write tests that resemble real usage
Mini Homework
- Create a simple TodoForm component with input + submit button
- Write tests:
- Renders correctly
- Typing updates input value
- Submit calls onSubmit with correct data
- Bonus: Test a custom hook like useFetch or useToggle
