React Testing — Jest & React Testing Library Guide
In this tutorial, you'll learn about React Testing. We cover key concepts, practical examples, and best practices.
React Testing Library combined with Jest lets you test React components by simulating how users interact with them — clicking buttons, typing in inputs, and verifying rendered output — without testing implementation details.
What You'll Learn
Test React components with Jest and React Testing Library — set up a testing environment, render components, fire events, test async operations, mock API calls, check accessibility, and measure test coverage.
Why It Matters
Untested React apps break silently — a refactored component, a changed API response, or a missing handler causes bugs that reach production. Tests catch these before deployment, document how components work, and give you confidence to refactor.
Real-World Use
A checkout form that must validate all fields before allowing submission (test every validation rule), a data table that sorts and filters (test with different data), or an auth flow that redirects based on login state.
Setting Up Jest and React Testing Library
Create React App includes both out of the box. For manual setup:
npm install --save-dev jest @testing-library/react @testing-library/jest-dom @testing-library/user-event
package.json (if not using Create React App):
{
"scripts": {
"test": "jest",
"test:coverage": "jest --coverage"
}
}
Configure Jest for React (optional, but recommended):
// jest.config.js
module.exports = {
testEnvironment: "jsdom",
transform: {
"^.+\\.(js|jsx)$": "babel-jest",
},
moduleNameMapper: {
"\\.(css|less|scss)$": "identity-obj-proxy",
},
setupFilesAfterSetup: ["@testing-library/jest-dom"],
};
Your First Test
// Greeting.jsx
function Greeting({ name }) {
return <h1>Hello, {name}!</h1>;
}
export default Greeting;
// Greeting.test.jsx
import { render, screen } from "@testing-library/react";
import Greeting from "./Greeting";
test("renders greeting with the given name", () => {
// Arrange — render the component
render(<Greeting name="Alice" />);
// Act — find the element
const heading = screen.getByText(/hello, alice/i);
// Assert — verify it exists
expect(heading).toBeInTheDocument();
});
How it works:
render(<Greeting name="Alice" />)renders the component into a virtual DOM.screen.getByText(/hello, alice/i)searches the rendered output for matching text (case-insensitive regex).expect(heading).toBeInTheDocument()asserts the element exists.
Expected output: The test passes, printing ✓ renders greeting with the given name.
Query Methods
React Testing Library provides several ways to find elements:
// By text content
screen.getByText("Submit"); // Exact text
screen.getByText(/submit/i); // Case-insensitive
// By role (preferred — accessible)
screen.getByRole("button", { name: /submit/i });
screen.getByRole("heading", { level: 2 });
screen.getByRole("textbox", { name: /email/i });
// By label (for form inputs)
screen.getByLabelText(/email/i);
// By test ID (last resort)
screen.getByTestId("submit-button");
// By placeholder
screen.getByPlaceholderText("Enter your name");
// Multiple matches
screen.getAllByRole("button"); // Returns array
screen.findAllByText("Item"); // Async variant
Preference order: getByRole > getByLabelText > getByText > getByTestId. Testing by role encourages accessible components.
Testing User Events
Use @testing-library/user-event for realistic event simulation (clicks, typing, etc.) instead of the basic fireEvent.
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
<button onClick={() => setCount(0)}>Reset</button>
</div>
);
}
test("increments and resets the counter", async () => {
const user = userEvent.setup();
render(<Counter />);
// Verify initial state
expect(screen.getByText("Count: 0")).toBeInTheDocument();
// Click increment twice
const incrementBtn = screen.getByRole("button", { name: /increment/i });
await user.click(incrementBtn);
await user.click(incrementBtn);
// Verify count updated
expect(screen.getByText("Count: 2")).toBeInTheDocument();
// Click reset
await user.click(screen.getByRole("button", { name: /reset/i }));
// Verify reset
expect(screen.getByText("Count: 0")).toBeInTheDocument();
});
Key userEvent methods:
user.click(element)— click an elementuser.type(element, "text")— type into an input (fires keystroke events)user.clear(element)— clear an inputuser.selectOptions(element, "option")— select from dropdownuser.tab()— simulate tabbing between elements
Testing Forms
function LoginForm({ onSubmit }) {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
function handleSubmit(e) {
e.preventDefault();
if (!email) { setError("Email is required"); return; }
if (!password) { setError("Password is required"); return; }
onSubmit({ email, password });
}
return (
<form onSubmit={handleSubmit}>
<label>
Email:
<input value={email} onChange={e => setEmail(e.target.value)} />
</label>
<label>
Password:
<input type="password" value={password} onChange={e => setPassword(e.target.value)} />
</label>
{error && <p role="alert">{error}</p>}
<button type="submit">Login</button>
</form>
);
}
test("shows validation error for empty email", async () => {
const user = userEvent.setup();
const mockSubmit = jest.fn();
render(<LoginForm onSubmit={mockSubmit} />);
// Click submit without filling fields
await user.click(screen.getByRole("button", { name: /login/i }));
// Verify error appears
expect(screen.getByRole("alert")).toHaveTextContent("Email is required");
expect(mockSubmit).not.toHaveBeenCalled();
});
test("submits form with valid data", async () => {
const user = userEvent.setup();
const mockSubmit = jest.fn();
render(<LoginForm onSubmit={mockSubmit} />);
// Fill in fields
await user.type(screen.getByLabelText(/email/i), "alice@example.com");
await user.type(screen.getByLabelText(/password/i), "secret123");
// Submit
await user.click(screen.getByRole("button", { name: /login/i }));
// Verify submit was called with correct data
expect(mockSubmit).toHaveBeenCalledWith({
email: "alice@example.com",
password: "secret123",
});
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
});
Async Testing
Components that fetch data or use timers need async testing.
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch(`https://api.example.com/users/${userId}`)
.then(r => r.json())
.then(data => {
setUser(data);
setLoading(false);
});
}, [userId]);
if (loading) return <p>Loading...</p>;
return <h1>{user.name}</h1>;
}
// Test with mock fetch
test("displays user name after loading", async () => {
// Mock the fetch API
global.fetch = jest.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve({ name: "Alice", id: 1 }),
});
render(<UserProfile userId={1} />);
// Loading state appears first
expect(screen.getByText("Loading...")).toBeInTheDocument();
// Wait for data to load
const userName = await screen.findByText("Alice");
expect(userName).toBeInTheDocument();
// Verify fetch was called correctly
expect(global.fetch).toHaveBeenCalledWith(
"https://api.example.com/users/1"
);
// Cleanup
delete global.fetch;
});
Async query methods:
findByText/findByRoleetc. — return a promise, resolve when element appears (timeout: 1000ms)findAllByText/findAllByRole— return promise resolving to arraywaitFor(() => expect(...))— wait for any assertion to passwaitForElementToBeRemoved(element)— wait for element to disappear
Mocking API Calls
For real projects, centralize API calls and mock at the module level.
// api.js
export async function fetchUser(id) {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) throw new Error("Failed to fetch");
return response.json();
}
// Profile.jsx
import { fetchUser } from "./api";
function Profile({ userId }) {
const [user, setUser] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
fetchUser(userId).then(setUser).catch(e => setError(e.message));
}, [userId]);
if (error) return <p>Error: {error}</p>;
if (!user) return <p>Loading...</p>;
return <h2>{user.name}</h2>;
}
// Profile.test.jsx
jest.mock("./api"); // Auto-mock the module
test("displays error when fetch fails", async () => {
const { fetchUser } = require("./api");
fetchUser.mockRejectedValue(new Error("Network error"));
render(<Profile userId={1} />);
const errorMsg = await screen.findByText(/network error/i);
expect(errorMsg).toBeInTheDocument();
});
Testing Mock Functions
test("mock function tracks calls", () => {
const mockFn = jest.fn();
mockFn("hello");
mockFn("world");
expect(mockFn).toHaveBeenCalledTimes(2);
expect(mockFn).toHaveBeenCalledWith("hello");
expect(mockFn).toHaveBeenCalledWith("world");
expect(mockFn).toHaveBeenLastCalledWith("world");
});
// Mock return values
const mockAdd = jest.fn().mockReturnValue(42);
expect(mockAdd()).toBe(42);
// Mock implementations
const mockFilter = jest.fn()
.mockImplementationOnce((items) => items.filter(Boolean))
.mockImplementationOnce((items) => items.slice(0, 3));
Testing Accessibility
React Testing Library encourages accessible components. Tests can verify ARIA attributes and roles.
function AccessibleButton({ label, onClick, disabled }) {
return (
<button
onClick={onClick}
disabled={disabled}
aria-label={label}
style={{
opacity: disabled ? 0.5 : 1,
padding: "8px 16px",
cursor: disabled ? "not-allowed" : "pointer",
}}
>
{label}
</button>
);
}
test("button has accessible label and respects disabled state", () => {
render(
<AccessibleButton label="Submit Form" onClick={() => {}} disabled />
);
const button = screen.getByRole("button", { name: /submit form/i });
expect(button).toBeDisabled();
expect(button).toHaveAttribute("aria-label", "Submit Form");
});
Measuring Coverage
npm test -- --coverage
This generates a coverage report showing:
- Statements: what percentage of code lines ran during tests
- Branches: if/else and ternary coverage
- Functions: function call coverage
- Lines: line execution coverage
Targets for production apps:
- Line coverage: 80%+
- Branch coverage: 70%+
- Critical paths (auth, checkout, forms): 100%
Coverage is tracked by tools like Durga Antivirus Pro for continuous quality monitoring of its UI components.
Common Errors in React Testing
1. Testing implementation details
// ❌ Bad: testing internal state
expect(container.querySelector("input").value).toBe("Alice");
// ✅ Good: testing what the user sees
expect(screen.getByDisplayValue("Alice")).toBeInTheDocument();
2. Forgetting await with userEvent
userEvent methods return promises. Without await, the events might not fire before assertions run.
3. Using getBy when element doesn't exist yet
Use findBy* (async) for elements that appear after data loads, or queryBy* (returns null instead of throwing) for elements that may not exist.
4. Not cleaning up mocks
Mocked modules persist across tests. Call jest.clearAllMocks() in beforeEach or use jest.resetAllMocks().
5. Testing too much in one test
Each test should verify one behavior. A test that clicks, types, submits, waits, and checks five things is hard to debug when it fails.
6. Forgetting act() warnings
React Testing Library wraps most APIs in act() automatically, but with custom timers or manual state updates, you might see act warnings. Use waitFor or act(() => { ... }).
7. Testing by CSS class
Classes change frequently during refactoring. Test by role, text, or label instead.
// ❌ Fragile
expect(button.classList.contains("btn-primary")).toBe(true);
// ✅ Robust
expect(screen.getByRole("button", { name: /submit/i })).toBeEnabled();
8. Not rendering with a Router
Components using useNavigate or Link need router context:
import { MemoryRouter } from "react-router-dom";
render(
<MemoryRouter>
<ComponentWithRouter />
</MemoryRouter>
);
Practice Questions
What is the difference between
getByText,queryByText, andfindByText?getByTextthrows if not found (synchronous).queryByTextreturns null if not found (synchronous).findByTextreturns a promise that resolves when found (asynchronous).Why should you use
getByRoleinstead ofgetByTestId?getByRoletests accessibility and encourages building components with proper ARIA attributes.getByTestIdshould be a last resort.How do you mock an API call in Jest? Mock the module with
jest.mock("./api")or mockglobal.fetchdirectly.What does
jest.fn()return? A mock function that records calls, arguments, and return values. You can chain.mockReturnValue(),.mockResolvedValue(), or.mockImplementation().How do you test a component that uses
useNavigate? Wrap it in<MemoryRouter>when rendering, then check that navigation happened by examining the URL or verifying the next page renders.
Challenge
Test a search component end-to-end:
// Build and test a SearchBox that:
// - Has an input field with placeholder "Search..."
// - Fetches results from /api/search?q={query} (debounced 300ms)
// - Shows "Loading..." while fetching
// - Shows results as a list
// - Shows "No results" when API returns empty array
// - Shows error message on API failure
// - Clears results when input is cleared
// Write tests for:
// 1. Input renders with correct placeholder
// 2. Typing triggers debounced API call
// 3. Loading state is shown during fetch
// 4. Results are displayed
// 5. Empty results show "No results" message
// 6. Error state is handled
Real-World Task: Form Testing Suite
Write a complete test suite for the validated registration form from the React Forms tutorial:
// Test cases:
// 1. Renders all form fields
// 2. Shows required error on submit with empty fields
// 3. Shows email format error for invalid email
// 4. Shows password length error for short password
// 5. Shows confirm password mismatch error
// 6. Submits successfully with valid data
// 7. Clears errors when user starts fixing a field
// 8. Submit button is disabled during submission
// 9. Shows success message after submission
// 10. Verifies the correct data was submitted
// Run with: npm test -- --coverage
// Target: 90%+ coverage on the form component
Learning Path
graph LR
A[React Basics] --> B[useState & useEffect]
B --> C[React Router & Forms]
C --> D[Performance Optimization]
D --> E[React Testing]
E --> F[Full-Stack React]
style E fill:#ff6b6b,stroke:#333,stroke-width:2px,color:#fff
Related Tutorials
- React Tutorial for Beginners — fundamentals before testing
- React Hooks Complete Guide — components you'll test
- useState Hook Guide — state patterns used in forms
- React Forms Guide — build the forms you'll test
- Learn about React Performance Optimization for profiling components
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro. Last updated: June 2026.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro