Testing React: Jest & React Testing Library
Write automated tests that click your components like a real user and check what shows on screen.
What you will learn
- Explain what a component test checks
- Render a component and query it like a user
- Simulate a click and assert the result
Why test components?
A test is a small piece of code that runs your component automatically and checks it behaves correctly — so you catch breakages without clicking through the app by hand every time. Two tools work together: Jest (or Vitest in Vite projects), the test runner that finds and runs your tests; and React Testing Library (RTL), which renders a component and lets you query it the way a user would — by the text and labels they see, not by internal details.
RTL’s golden rule: test what the user sees and does, not how the component is built inside. That way your tests keep passing when you refactor, and only fail when behaviour actually breaks.
Setup
npm install -D vitest @testing-library/react @testing-library/jest-dom @testing-library/user-event jsdomThese dev-dependencies give you the runner (vitest, used just like Jest), RTL to render components, jest-dom for friendly checks like “is this on screen”, user-event to simulate real clicks and typing, and jsdom (a fake browser environment so tests can run without a real browser). This is real project code, shown with its Output.
The component under test
// Counter.jsx
import { useState } from 'react';
export function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}This Counter component shows a count and an Increment button that adds 1 on each click — intentionally tiny so the test is easy to follow. We will check two behaviours: that it first renders Count: 0, and that clicking the button updates the number to 1.
The test
// Counter.test.jsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Counter } from './Counter.jsx';
test('starts at 0 and increments when clicked', async () => {
render(<Counter />);
// 1) it starts at 0 (find the text the user sees)
expect(screen.getByText('Count: 0')).toBeInTheDocument();
// 2) click the button by its visible label
await userEvent.click(screen.getByRole('button', { name: 'Increment' }));
// 3) the count is now 1
expect(screen.getByText('Count: 1')).toBeInTheDocument();
});A test reads like a story of a user interacting with the component:
- Render it:
render(<Counter />)mounts the component into the fake browser so we can interact with it. - Query like a user:
screen.getByText('Count: 0')finds the element by the text a person would read.expect(...).toBeInTheDocument()is the assertion — it says “I expect this to be on screen”. If it is not, the test fails. - Act:
userEvent.click(screen.getByRole('button', { name: 'Increment' }))finds the button by its visible label and clicks it, exactly like a user would. Weawaitit because the click is asynchronous. - Assert the result: after the click we expect
Count: 1to be on screen. If the component is broken, this line fails and tells you.
Note: Output (from the test runner): ✓ starts at 0 and increments when clicked (23 ms) Test Files 1 passed (1) Tests 1 passed (1) If the increment logic were broken, you would instead see a red ✗ with “Unable to find an element with the text: Count: 1”.
How to query — prefer what users perceive
| Query | Finds elements by |
|---|---|
getByRole('button', { name }) | Role + visible label (preferred) |
getByText('…') | Visible text content |
getByLabelText('…') | A form field by its <label> |
getByTestId('…') | A data-testid (last resort) |
Tip: Beyond unit/component tests, larger teams add end-to-end (E2E) tests with Playwright or Cypress — these drive a real browser through whole user journeys (log in, add to cart, check out). Start with RTL component tests; reach for E2E for critical flows.
Watch out: Avoid testing internal details (state variable names, exact CSS classes). Test the behaviour the user experiences. Detail-coupled tests break on every refactor and slow the team down.
Q. What is React Testing Library’s core philosophy?
✍️ Practice
- Write a test that renders a
Greetingcomponent and asserts the greeting text is on screen. - Write a test that types into a controlled input and asserts a live preview updates.
🏠 Homework
- Add a test for your to-do app: render it, add a task by clicking Add, and assert the new task appears in the list.