Skip to content

React Testing — Jest & React Testing Library Guide

DodaTech 10 min read

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:

  1. render(<Greeting name="Alice" />) renders the component into a virtual DOM.
  2. screen.getByText(/hello, alice/i) searches the rendered output for matching text (case-insensitive regex).
  3. 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 element
  • user.type(element, "text") — type into an input (fires keystroke events)
  • user.clear(element) — clear an input
  • user.selectOptions(element, "option") — select from dropdown
  • user.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 / findByRole etc. — return a promise, resolve when element appears (timeout: 1000ms)
  • findAllByText / findAllByRole — return promise resolving to array
  • waitFor(() => expect(...)) — wait for any assertion to pass
  • waitForElementToBeRemoved(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

  1. What is the difference between getByText, queryByText, and findByText? getByText throws if not found (synchronous). queryByText returns null if not found (synchronous). findByText returns a promise that resolves when found (asynchronous).

  2. Why should you use getByRole instead of getByTestId? getByRole tests accessibility and encourages building components with proper ARIA attributes. getByTestId should be a last resort.

  3. How do you mock an API call in Jest? Mock the module with jest.mock("./api") or mock global.fetch directly.

  4. What does jest.fn() return? A mock function that records calls, arguments, and return values. You can chain .mockReturnValue(), .mockResolvedValue(), or .mockImplementation().

  5. 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
  • 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