Unit Testing in React – Complete Guide with Examples (2025)
The most popular and recommended way to unit test React components today is using React Testing Library (RTL) along with Jest.
React Testing Library focuses on testing components the way users interact with them (by accessibility, labels, text, roles — not by implementation details).
1. Basic Setup (create-react-app or Vite)
Bash
npm install --save-dev @testing-library/react @testing-library/jest-dom @testing/user-event jest
Add to your setupTests.js or vitest.setup.ts:
JavaScript
import '@testing-library/jest-dom'
2. Simple Example: Testing a Counter Component
jsx
// Counter.jsx
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<h1 data-testid="title">My Counter</h1>
<p aria-label="count">Current count: {count}</p>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
<button onClick={() => setCount(c => c - 1)}>Decrement</button>
</div>
);
}
jsx
// Counter.test.jsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Counter from './Counter';
test('renders counter and increments/decrements correctly', async () => {
const user = userEvent.setup();
render(<Counter />);
// 1. By visible text
expect(screen.getByText('My Counter')).toBeInTheDocument();
expect(screen.getByText(/current count: 0/i)).toBeInTheDocument();
// 2. By role + name (recommended)
const incrementBtn = screen.getByRole('button', { name: /increment/i });
const decrementBtn = screen.getByRole('button', { name: /decrement/i });
await user.click(incrementBtn);
await user.click(incrementBtn);
expect(screen.getByText(/current count: 2/i)).toBeInTheDocument();
await user.click(decrementBtn);
expect(screen.getByText(/current count: 1/i)).toBeInTheDocument();
});
All Ways to Query / Identify Elements in React Testing Library
| Query Type | Recommended | Example | When to Use |
|---|---|---|---|
| getByRole | ★★★★★ | screen.getByRole(‘button’, { name: ‘Submit’ }) | Most cases – accessible |
| getByLabelText | ★★★★★ | screen.getByLabelText(‘Username:’) | Form fields |
| getByPlaceholderText | ★★★★ | screen.getByPlaceholderText(‘Enter email’) | When no label |
| getByText | ★★★★ | screen.getByText(‘Welcome, John’) | Visible text |
| getByDisplayValue | ★★★★ | screen.getByDisplayValue(‘john@example.com’) | Input values |
| getByAltText | ★★★★★ | screen.getByAltText(‘Profile picture’) | Images |
| getByTitle | ★★★★ | screen.getByTitle(‘Close’) | Tooltips |
| getByTestId | ★★ | screen.getByTestId(‘custom-id’) → <div data-testid=”custom-id”> | Last resort |
| getByAriaLabel (custom) | – | Use getByRole instead whenever possible | – |
Query Variants (important!)
For every query above, there are 4 variants:
| Variant | Throws if… | Returns | Use When |
|---|---|---|---|
| getBy… | 0 or >1 elements | 1 element | Expect exactly one |
| getAllBy… | 0 elements | Array | Multiple elements |
| queryBy… | Never throws (returns null) | Element or null | Check absence |
| queryAllBy… | Never throws | Array (empty if none) | Find multiple or none |
| findBy… | Not found within timeout (async) | Promise → element | Async content (API calls) |
| findAllBy… | Not found within timeout | Promise → array | Multiple async |
Best Practices (2025)
JavaScript
// GOOD
screen.getByRole('button', { name: 'Save' })
screen.getByLabelText('Password')
// AVOID (tests become brittle)
screen.getByTestId('submit-btn') // only when no accessible way
container.querySelector('.css-abc') // never
Testing Custom Hooks (with @testing-library/react-hooks)
Bash
npm install --save-dev @testing-library/react-hooks
JavaScript
// useCounter.js
import { useState } from 'react';
export function useCounter(initial = 0) {
const [count, setCount] = useState(initial);
return { count, increment: () => setCount(c => c + 1), decrement: () => setCount(c => c - 1) };
}
// useCounter.test.js
import { renderHook, act } from '@testing-library/react-hooks';
import { useCounter } from './useCounter';
test('should increment and decrement', () => {
const { result } = renderHook(() => useCounter(10));
act(() => result.current.increment());
expect(result.current.count).toBe(11);
act(() => result.current.decrement());
expect(result.current.count).toBe(10);
});
Summary: Prefer queries in this order
- getByRole
- getByLabelText
- getByPlaceholderText
- getByText
- getByDisplayValue
- getByAltText
- getByTitle
- getByTestId → only when nothing else works
This approach makes your tests resilient to refactoring and mirrors real user behavior.
@testing-library/jest-dommatchers (DOM-specific, by far the most common in modern React projects)- A few essential built-in Jest matchers that pair with them
Top 10–12 Most Frequently Used Matchers (with short explanations)
.toBeInTheDocument()
Checks if the element exists in the DOM.
→expect(screen.getByText('Welcome')).toBeInTheDocument().toHaveTextContent('text' | /regex/)
Verifies the visible text content (very common).
→expect(button).toHaveTextContent('Submit')
→expect(card).toHaveTextContent(/price: \$?99/i).toBeVisible()
Element is visible (not hidden by CSS, opacity, visibility, etc.).
→ Great for conditional rendering checks..toBeDisabled()/.toBeEnabled()
Checks disabled/enabled state of buttons, inputs, etc. (form + UX testing).
→expect(submitBtn).toBeDisabled().toHaveClass('class-name')
Verifies CSS class presence (styling/state).
→expect(element).toHaveClass('active')
→ Can take multiple:.toHaveClass('btn', 'primary').toHaveAttribute('attr', 'value?')
Checks for attributes (very common withdata-testid,aria-*, etc.).
→expect(input).toHaveAttribute('placeholder', 'Enter name').toHaveValue('value')
Checks form field value (input, textarea, select).
→expect(input).toHaveValue('john@example.com').toBeChecked()/.toBePartiallyChecked()
For checkboxes, radios, switches.
→expect(checkbox).toBeChecked().toHaveFocus()
Element has focus (after interactions).
→expect(input).toHaveFocus().toBeRequired()
Form field is required.
→expect(input).toBeRequired().toHaveAccessibleName('name')
Accessibility: element has correct accessible name (button, input, etc.).
→ Increasingly important in 2025–2026 projects..toHaveStyle({ color: 'red' })
Inline or computed style check.
→expect(el).toHaveStyle({ display: 'none' })
Bonus: Still-essential built-in Jest matchers (used together with the above)
.toEqual(obj/array)→ deep equality (props, state, returned values).toBe(value)→ strict equality (primitives).toHaveBeenCalledWith(...)→ mock function calls (event handlers).not.prefix → negation (.not.toBeDisabled(),.not.toBeInTheDocument())
Quick “Top 5” cheat-sheet most devs use daily
expect(el).toBeInTheDocument()
expect(el).toHaveTextContent('...')
expect(btn).toBeEnabled() // or .toBeDisabled()
expect(input).toHaveValue('...')
expect(el).toHaveAttribute('data-testid', 'hero')
These cover the vast majority of assertions in component + form + accessibility tests.
If you’re starting fresh in 2026, install @testing-library/jest-dom and add to your setup:
import '@testing-library/jest-dom';
There are two main groups:
- Built-in Jest matchers (available out of the box with Jest)
- @testing-library/jest-dom matchers (very common in React/RTL projects — you usually add
import '@testing-library/jest-dom'in your setup file)
1. Core Jest Matchers (most frequently used)
expect(value).toBe(5) // strict equality (===)
expect(value).toEqual({ a: 1 }) // deep equality
expect(value).toBeTruthy() // true, 1, "hello", objects, arrays...
expect(value).toBeFalsy() // false, 0, "", null, undefined...
expect(value).toBeNull()
expect(value).toBeUndefined()
expect(value).toBeDefined()
expect(value).toBeGreaterThan(10)
expect(value).toBeGreaterThanOrEqual(10)
expect(value).toBeLessThan(100)
expect(value).toBeLessThanOrEqual(100)
expect(value).toMatch(/regex/) // string regex match
expect(value).toMatch('substring') // string contains
expect(value).toContain('item') // array contains item
expect(array).toContainEqual({ x: 1 }) // deep equality in array
expect(value).toHaveLength(3) // string / array length
expect(object).toHaveProperty('name')
expect(fn).toThrow() // throws any error
expect(fn).toThrow('specific message')
expect(fn).toThrow(Error)
expect(mockFn).toHaveBeenCalled()
expect(mockFn).toHaveBeenCalledTimes(2)
expect(mockFn).toHaveBeenCalledWith('arg1', 42)
expect(mockFn).toHaveBeenLastCalledWith(...)
expect(mockFn).toHaveBeenNthCalledWith(2, ...)
expect(value).not.toBe(5) // negation (works with almost all matchers)
2. DOM-specific matchers from @testing-library/jest-dom
(these are the ones people usually mean when testing React components)
// Most popular ones
expect(element).toBeInTheDocument()
expect(element).not.toBeInTheDocument()
expect(element).toBeVisible() // not hidden via CSS
expect(element).toBeHidden()
expect(element).toHaveTextContent('hello world')
expect(element).toHaveTextContent(/hello/i) // regex
expect(element).toHaveTextContent('exact', { normalizeWhitespace: false })
expect(element).toContainElement(childElement)
expect(element).toHaveClass('btn')
expect(element).toHaveClass('primary', 'large') // multiple classes
expect(element).not.toHaveClass('disabled')
expect(element).toHaveAttribute('data-testid', 'submit-btn')
expect(element).toHaveAttribute('disabled')
expect(element).toBeDisabled()
expect(element).toBeEnabled()
expect(element).toBeRequired()
expect(element).toBeChecked() // checkboxes & radio
expect(element).toBePartiallyChecked()
expect(element).toHaveFocus()
expect(element).toHaveStyle({ color: 'red', fontSize: '16px' })
expect(element).toHaveStyle('color: red') // string form
expect(element).toHaveFormValue('username', 'john') // form fields
Quick reference – most common pattern in React Testing Library
import { render, screen } from '@testing-library/react'
import '@testing-library/jest-dom'
test('shows button', () => {
render(<Button>Click me</Button>)
const btn = screen.getByRole('button', { name: /click me/i })
expect(btn).toBeInTheDocument()
expect(btn).toBeEnabled()
expect(btn).toHaveClass('btn-primary')
expect(btn).toHaveTextContent('Click me')
})
Summary – the ones you’ll use 90% of the time
.toBeInTheDocument().toHaveTextContent().toBeVisible().toHaveClass().toBeDisabled().toBeEnabled().toBeChecked().toHaveAttribute().toHaveStyle().toBe() / .toEqual().toHaveBeenCalledWith()
For the complete up-to-date list:
- Jest built-in → https://jestjs.io/docs/expect
- jest-dom matchers → https://github.com/testing-library/jest-dom
Happy testing
