Testing React components feels a lil different from testing regular functions. You're not checking return values anymore. You're checking what the user SEES, and how the component reacts when they poke at it. The golden rule here is simple... test your components the way a real person would use them.
React Testing Library Philosophy
React Testing Library is built on one simple idea. If your tests use your components the same way people do, your tests end up more reliable and a lot more meaningful.
So instead of poking at internal state or behind-the-scenes details, you do this.
Find elements by their text, labels, or roles (how users find them)
Click buttons, type in inputs, submit forms (how users interact)
Check what's actually visible on screen (what users see)
Setting It Up
React Testing Library needs a fake browser to render into, plus a few extra packages. Install them as dev dependencies.
Skip the setup file and every toBeInTheDocument check blows up, because Vitest doesn't ship those matchers on its own.
Here's a first test for a lil Counter component (one that shows Count: 0 and has an Increment button, like the one from the React module). The test has JSX in it, so name the file Counter.test.tsx and not .ts.
Example
import { render, screen, fireEvent } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import { Counter } from './Counter';
describe('Counter', () => {
it('should increment when the button is clicked', () => {
render(<Counter />);
// Find the button like a user would
const button = screen.getByRole('button', { name: /increment/i });
// Click it like a user would
fireEvent.click(button);
// Check what the user would see
expect(screen.getByText('Count: 1')).toBeInTheDocument();
});
});
The Render, Screen, and FireEvent Trio
These three tools cover most of what you'll do when testing React.
Step 1. render() - Put the Component on Screen
The render function puts your component on a simulated browser page that lives in memory. Think of it as loading a web page in an invisible browser.
Step 2. screen - Find Elements
The screen object is how you go looking for elements. You'll use queries like getByText, getByRole, getByLabelText, or getByPlaceholderText.
Step 3. fireEvent - Simulate Interactions
The fireEvent object lets you fake clicks, typing, form submissions, and whatever else a user might do.
Finding Elements
Picking the right query matters. Here's the order Testing Library recommends you try them in.
Example
// Best: Accessible to everyone (screen readers, etc.)
screen.getByRole('button', { name: 'Submit' });
screen.getByLabelText('Email address');
// Good: Visible text
screen.getByText('Welcome back!');
screen.getByPlaceholderText('Enter your name');
// Okay: Test utilities
screen.getByTestId('custom-element');
Testing User Interactions
Here's a fuller example that tests a login form.
Example
import { render, screen, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, it, expect, vi } from 'vitest';
import { LoginForm } from './LoginForm';
describe('LoginForm', () => {
it('should show error message for empty email', async () => {
render(<LoginForm onSubmit={vi.fn()} />);
// Click submit without entering email
const submitButton = screen.getByRole('button', { name: /sign in/i });
fireEvent.click(submitButton);
// Error message should appear
expect(screen.getByText(/email is required/i)).toBeInTheDocument();
});
it('should call onSubmit with form data', async () => {
const user = userEvent.setup();
const handleSubmit = vi.fn();
render(<LoginForm onSubmit={handleSubmit} />);
// Fill in the form
const emailInput = screen.getByLabelText(/email/i);
const passwordInput = screen.getByLabelText(/password/i);
await user.type(emailInput, 'alice@example.com');
await user.type(passwordInput, 'password123');
// Submit the form
await user.click(screen.getByRole('button', { name: /sign in/i }));
// Check the callback was called correctly
expect(handleSubmit).toHaveBeenCalledWith({
email: 'alice@example.com',
password: 'password123'
});
});
});
See that userEvent.setup() at the top of the second test? That's the way the Testing Library docs recommend using userEvent today. Make one user per test, then await every click and keystroke it does.
Testing Async Behavior
Lots of components fetch data or wait around for something to finish. For that async content, use waitFor or findBy queries.
Example
describe('UserProfile', () => {
it('should display user data after loading', async () => {
render(<UserProfile userId="123" />);
// Initially shows loading state
expect(screen.getByText(/loading/i)).toBeInTheDocument();
// Wait for the data to appear
const userName = await screen.findByText('Alice Johnson');
expect(userName).toBeInTheDocument();
// Loading should be gone
expect(screen.queryByText(/loading/i)).not.toBeInTheDocument();
});
});
Testing Component Props
Check that your components render the right way with different props.
The first time this test runs, it saves a snapshot. Every run after that compares against the saved copy. If the output changes, the test fails, and then it's on you to decide whether that change was on purpose.
TL;DR
Test components the way real users use them
Reach for getByRole and getByLabelText first, since they're the accessible queries
Use fireEvent or userEvent to fake user interactions
Don't test behind-the-scenes details like internal state
What's Next?
Tests catch a LOT, but sooner or later something's gonna break anyway. When it does, you need a plan. Next up is debugging techniques, so you can track bugs down and squash them without losing your mind...