How to test components in React with RTL
Testing components from a user’s perspective ensures your application is accessible and behaves as users expect. With over 12 years of React development experience since 2014 and as the creator of CoreUI, I’ve adopted React Testing Library as the standard for component testing. React Testing Library (RTL) encourages testing components the way users interact with them, focusing on accessibility and user behavior. This approach creates tests that are more maintainable and closely aligned with real-world usage.
Use React Testing Library to test components with user-centric queries and interaction testing.
Install React Testing Library:
npm install --save-dev @testing-library/react @testing-library/jest-dom @testing-library/user-event
Basic component testing:
// LoginForm.jsx
import { useState } from 'react'
export default function LoginForm({ onSubmit }) {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState('')
const handleSubmit = (e) => {
e.preventDefault()
if (!email || !password) {
setError('All fields are required')
return
}
onSubmit({ email, password })
}
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor='email'>Email</label>
<input
id='email'
type='email'
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<div>
<label htmlFor='password'>Password</label>
<input
id='password'
type='password'
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
{error && <div role='alert'>{error}</div>}
<button type='submit'>Login</button>
</form>
)
}
Test file:
// LoginForm.test.jsx
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import LoginForm from './LoginForm'
describe('LoginForm', () => {
it('renders form fields', () => {
render(<LoginForm onSubmit={jest.fn()} />)
expect(screen.getByLabelText(/email/i)).toBeInTheDocument()
expect(screen.getByLabelText(/password/i)).toBeInTheDocument()
expect(screen.getByRole('button', { name: /login/i })).toBeInTheDocument()
})
it('submits form with valid data', async () => {
const user = userEvent.setup()
const handleSubmit = jest.fn()
render(<LoginForm onSubmit={handleSubmit} />)
await user.type(screen.getByLabelText(/email/i), '[email protected]')
await user.type(screen.getByLabelText(/password/i), 'password123')
await user.click(screen.getByRole('button', { name: /login/i }))
expect(handleSubmit).toHaveBeenCalledWith({
email: '[email protected]',
password: 'password123'
})
})
it('shows error for empty fields', async () => {
const user = userEvent.setup()
render(<LoginForm onSubmit={jest.fn()} />)
await user.click(screen.getByRole('button', { name: /login/i }))
expect(screen.getByRole('alert')).toHaveTextContent('All fields are required')
})
})
Query priority (use in this order):
// 1. Accessible to everyone
screen.getByRole('button', { name: /submit/i })
screen.getByLabelText(/email/i)
screen.getByPlaceholderText(/search/i)
screen.getByText(/welcome/i)
// 2. Semantic queries
screen.getByAltText(/profile picture/i)
screen.getByTitle(/close/i)
// 3. Test IDs (last resort)
screen.getByTestId('custom-element')
Testing async operations:
import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
it('loads and displays data', async () => {
render(<DataComponent />)
expect(screen.getByText(/loading/i)).toBeInTheDocument()
await waitFor(() => {
expect(screen.getByText(/data loaded/i)).toBeInTheDocument()
})
})
it('handles API errors', async () => {
const user = userEvent.setup()
render(<FormComponent />)
await user.click(screen.getByRole('button', { name: /submit/i }))
expect(await screen.findByText(/error occurred/i)).toBeInTheDocument()
})
Testing user interactions:
it('toggles visibility', async () => {
const user = userEvent.setup()
render(<ToggleComponent />)
const button = screen.getByRole('button')
expect(screen.queryByText(/hidden content/i)).not.toBeInTheDocument()
await user.click(button)
expect(screen.getByText(/hidden content/i)).toBeInTheDocument()
await user.click(button)
expect(screen.queryByText(/hidden content/i)).not.toBeInTheDocument()
})
Best Practice Note
React Testing Library encourages testing how users interact with your app, not implementation details. Use getByRole as your primary query—it ensures accessibility. Use userEvent instead of fireEvent for more realistic interactions. Async queries like findBy and waitFor handle loading states. Use queryBy to assert element absence. Avoid testing internal state—test outputs and side effects instead. This is how we test CoreUI React components—focusing on user experience, accessibility, and real-world interactions rather than implementation details, resulting in more maintainable and reliable tests.



