Testing React Applications: Complete Guide for 2024
Master React testing with Jest, React Testing Library, Vitest, and best practices for unit, integration, and end-to-end testing.
3 min read
By Sagar Tank
TestingReactJestReact Testing LibraryVitestTDD
T
Image

Testing is essential for building reliable React applications. A comprehensive testing strategy ensures your code works correctly and prevents regressions.
Testing Pyramid
- Unit Tests (70%) - Test individual functions and components
- Integration Tests (20%) - Test component interactions
- E2E Tests (10%) - Test complete user flows
Setting Up Testing
Jest + React Testing Library
pnpm add -D @testing-library/react @testing-library/jest-dom jest jest-environment-jsdom
Vitest (Modern Alternative)
pnpm add -D vitest @testing-library/react @testing-library/jest-dom
Component Testing
Basic Component Test
import { render, screen } from '@testing-library/react'
import { Button } from './Button'
test('renders button with text', () => {
render(<Button>Click me</Button>)
expect(screen.getByRole('button', { name: /click me/i })).toBeInTheDocument()
})
Testing User Interactions
import { render, screen, fireEvent } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
test('button click increments counter', async () => {
const user = userEvent.setup()
render(<Counter />)
const button = screen.getByRole('button', { name: /increment/i })
await user.click(button)
expect(screen.getByText(/count: 1/i)).toBeInTheDocument()
})
Testing Async Operations
import { render, screen, waitFor } from '@testing-library/react'
test('loads and displays user data', async () => {
render(<UserProfile userId="123" />)
expect(screen.getByText(/loading/i)).toBeInTheDocument()
await waitFor(() => {
expect(screen.getByText(/john doe/i)).toBeInTheDocument()
})
})
Mocking
Mock Functions
import { vi } from 'vitest'
test('calls onSubmit with form data', async () => {
const handleSubmit = vi.fn()
render(<Form onSubmit={handleSubmit} />)
await userEvent.type(screen.getByLabelText(/email/i), 'test@example.com')
await userEvent.click(screen.getByRole('button', { name: /submit/i }))
expect(handleSubmit).toHaveBeenCalledWith({
email: 'test@example.com'
})
})
Mock API Calls
import { vi } from 'vitest'
vi.mock('./api', () => ({
fetchUser: vi.fn(() => Promise.resolve({ name: 'John Doe' }))
}))
Testing Custom Hooks
import { renderHook, act } from '@testing-library/react'
import { useCounter } from './useCounter'
test('increments counter', () => {
const { result } = renderHook(() => useCounter())
act(() => {
result.current.increment()
})
expect(result.current.count).toBe(1)
})
Testing Forms
test('validates and submits form', async () => {
const user = userEvent.setup()
render(<ContactForm />)
const emailInput = screen.getByLabelText(/email/i)
const submitButton = screen.getByRole('button', { name: /submit/i })
// Test validation
await user.click(submitButton)
expect(screen.getByText(/email is required/i)).toBeInTheDocument()
// Test submission
await user.type(emailInput, 'test@example.com')
await user.click(submitButton)
await waitFor(() => {
expect(screen.getByText(/thank you/i)).toBeInTheDocument()
})
})
Best Practices
- Test behavior, not implementation - Focus on what users see and do
- Use accessible queries - Prefer
getByRole,getByLabelText - Keep tests simple - One assertion per test when possible
- Test user flows - Test how users interact with your app
- Mock external dependencies - Isolate your components
Coverage Goals
- Statements: 80%+
- Branches: 75%+
- Functions: 80%+
- Lines: 80%+
Conclusion
A solid testing strategy gives you confidence in your code. Start with component tests, add integration tests for critical flows, and use E2E tests sparingly for key user journeys.
Enjoyed this article?
Check out more articles on the blog or get in touch to discuss your project.