C.W.K.
Stream
Lesson 01 of 04 · published

Wiring Up React Testing Library

~16 min · vitest-components, rtl, setup

Level 0Test Curious
0 XP0/32 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete
"Render, query, assert. The whole library is three verbs and a philosophy."

What React Testing Library Is

RTL gives you three small things: a render() function that mounts a component in a DOM, a screen object that queries the rendered DOM, and a set of conventions about what kinds of queries you should prefer. It's deliberately tiny — no shallow render, no instance access, no internal-state probing. The premise: if your test queries the DOM the way users (and assistive tech) do, the test will catch real bugs and survive real refactors.

RTL pairs with @testing-library/jest-dom for assertions like toBeInTheDocument(), toHaveClass(), toBeDisabled() — and with @testing-library/user-event for realistic interactions (next lesson).

The Install

Three packages cover the React side. Plus happy-dom (from the Vitest install in lesson 2.1) which provides the DOM RTL queries against.

The Setup File

Two things go in vitest.setup.ts:

  1. import '@testing-library/jest-dom/vitest' — exposes the custom matchers (toBeInTheDocument, etc.) globally.
  2. An afterEach calling cleanup() from @testing-library/react — unmounts components between tests so they don't bleed into each other. If you set globals: true in vitest.config or you're on RTL v15+, cleanup runs automatically — but explicit is safer than implicit when test isolation matters.
Test what a user sees, not what the component holds. If your test reaches for a prop, a state value, or a ref, ask: would a user know that thing exists? If no, your test is overfit to the implementation. Re-write to query the visible DOM.

The First Component Test

The skeleton is uniform: render(<Component />), then screen.getByX(...) to find an element, then expect(element).toBeInTheDocument() or similar. Read the test as a story — a user opens the page, sees a heading, clicks a button, sees a new state.

Common Pitfalls in Setup

  • Matchers not loaded: toBeInTheDocument is not a function means you forgot the '@testing-library/jest-dom/vitest' import in vitest.setup.ts.
  • Tests pollute each other: a query in one test finds an element rendered by the previous one. Add explicit cleanup() in afterEach (or upgrade RTL — v15+ auto-cleans).
  • CSS-in-JS errors: happy-dom doesn't fully implement every CSS-related API. If a component crashes on render with a CSS error, switch the environment to jsdom just for that file with // @vitest-environment jsdom at the top.

Code

Install — three RTL packages·bash
# RTL + matchers + user-event
npm install -D \
  @testing-library/react \
  @testing-library/jest-dom \
  @testing-library/user-event

# happy-dom should already be installed from the Vitest setup lesson
vitest.setup.ts — matchers + cleanup·typescript
// vitest.setup.ts
import '@testing-library/jest-dom/vitest';
import { cleanup } from '@testing-library/react';
import { afterEach } from 'vitest';

// Unmount React trees between tests so queries don't see stale DOM.
afterEach(() => {
  cleanup();
});
The component under test·tsx
// src/components/greeting.tsx
export function Greeting({ name }: { name: string }) {
  return (
    <section aria-labelledby="greeting-heading">
      <h2 id="greeting-heading">Welcome, {name}.</h2>
      <p>Glad you made it.</p>
    </section>
  );
}
First RTL test — render, query, assert·tsx
// src/components/greeting.test.tsx
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import { Greeting } from './greeting';

describe('<Greeting />', () => {
  it('renders the heading with the user name', () => {
    render(<Greeting name="Pippa" />);

    const heading = screen.getByRole('heading', { name: /welcome, pippa/i });
    expect(heading).toBeInTheDocument();
  });

  it('renders the supporting text', () => {
    render(<Greeting name="Pippa" />);

    expect(screen.getByText(/glad you made it/i)).toBeInTheDocument();
  });
});

External links

Exercise

Build a <UserCard user={{ name, email, online }} /> component that shows the name in a heading, the email as a link (mailto:), and an 'Online' badge when online === true. Write three tests using screen.getByRole: (1) the heading appears with the name, (2) the email link has the right href, (3) the badge appears only when online is true (use queryByText for the absent case so it returns null instead of throwing).
Hint
For (3), pair queryByText('Online') with .not.toBeInTheDocument(). Using getByText would throw before reaching the assertion — queryBy returns null when absent, which is what not.toBeInTheDocument expects.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.