Testing with Vitest & React Testing Library
Set up Vitest, query the DOM like a user, simulate events, test async UI and mock fetch.
Tests let you change code with confidence. Instead of clicking through your app after every edit, you write small programs that do the clicking for you and check the results. In the React world the standard tools are Vitest (a fast test runner that shares your Vite config) and React Testing Library (RTL), which renders components and lets you interact with them the way a user would.
The guiding principle#
"The more your tests resemble the way your software is used, the more confidence they can give you." — Kent C. Dodds, creator of Testing Library
So don't test implementation details (state variable names, which hooks you used). Test behaviour: what the user sees and what happens when they interact. Then you can refactor freely without breaking tests.
Setup#
Add a script to package.json:
npm test runs in watch mode, re-running affected tests as you save. npx vitest run runs once (for CI). Vitest finds files named *.test.jsx, *.test.tsx and so on.
Your first test#
Run npx vitest run --reporter=verbose to see each test by name:
The pattern is always Arrange (render), Act (interact), Assert (expect).
Finding elements#
screen exposes queries that search the rendered page. Prefer them in this order:
getByRole—getByRole("button", { name: "Save" }),getByRole("heading", { level: 1 }),getByRole("textbox", { name: "Email" })getByLabelText— form fields by their<label>getByPlaceholderText,getByText,getByDisplayValuegetByAltText,getByTitlegetByTestId— last resort, for elements with no accessible handle (data-testid="chart")
Each comes in three flavours:
Add All for multiple matches: getAllByRole("listitem").
Tip: run
screen.debug()to print the current DOM, orscreen.logTestingPlaygroundURL()to get a link that suggests the best query for each element.
Testing a form#
vi.fn() creates a mock function that records how it was called.
Testing async code and mocking fetch#
Tests shouldn't depend on a real server — it's slow and flaky. Replace fetch with a fake:
For bigger apps, Mock Service Worker (MSW) intercepts requests at the network level, so the same mocks work in tests and in the browser.
Testing custom hooks#
Most hooks are best tested through a component that uses them. For reusable hooks, use renderHook:
Components that need providers#
If components use a router, React Query or context, wrap them in a helper:
For React Router, render your routes with createMemoryRouter(routes, { initialEntries: ["/courses/react"] }) and <RouterProvider>.
Timers#
Useful jest-dom matchers#
toBeInTheDocument(), toBeVisible(), toBeDisabled(), toHaveTextContent(), toHaveValue(), toBeChecked(), toHaveAttribute(), toHaveClass(), toHaveFocus(), toHaveAccessibleName().
What to test#
- Do test user-visible behaviour: rendering, interactions, validation, loading/error states, edge cases like empty lists.
- Do unit-test pure logic (reducers, formatters) directly — they're the cheapest tests.
- Don't test that React works (that
useStateupdates) or snapshot huge components. - Add end-to-end tests (Playwright) for a few critical flows like sign-up and checkout, running in real browsers.
Common mistakes#
- Forgetting
awaitonuser.click/user.type, or onfindByqueries. - Using
getByto assert absence (it throws) instead ofqueryBy. - Querying by class names or component internals, so tests break on harmless refactors.
- Hitting real APIs in tests.
- Wrapping everything in
act()— RTL'srenderanduserEventalready do it.
What's next#
Tests prove your app works; next make sure it works fast. Up next: performance and React DevTools.
Check your understanding
Quick quiz
1.Which React Testing Library query should you usually prefer?
2.What is the difference between
getBy…andfindBy…queries?3.Why use
userEventinstead offireEvent?
Finished reading?
Mark this lesson complete to track your progress.