Testing Project
โš›๏ธ React 18+ ๐ŸŸข Chapter 35 of 39 ๐Ÿ“‚ Phase 15: Testing ๐Ÿ“… 2026 Edition
๐Ÿ“Œ Covered in this chapter: Test a Button ยท Test a Counter ยท Test a Form ยท Test Validation ยท Test Loading State ยท Test Error State ยท Test API Data
This chapter applies Chapter 34's tools to a series of realistic, self-contained testing exercises, covering the most common patterns you'll actually use across a real project.
1Testing a Button's Click Handler
๐Ÿ’ป Example 1: Testing That a Callback Prop Fires
JSX (Vitest)
import { render, screen, fireEvent } from "@testing-library/react";
import { test, expect, vi } from "vitest";

function DeleteButton({ onDelete }) {
  return <button onClick={onDelete}>Delete</button>;
}

test("calls onDelete when clicked", () => {
  const handleDelete = vi.fn();   // a mock/fake function that tracks its own calls
  render(<DeleteButton onDelete={handleDelete} />);

  fireEvent.click(screen.getByText("Delete"));

  expect(handleDelete).toHaveBeenCalledTimes(1);
});

vi.fn() creates a mock function specifically so the test can verify it was called, without needing any real deletion logic โ€” perfect for testing Chapter 17's callback-prop pattern in isolation from whatever the parent actually does with it.

2Testing Form Validation States
test("shows an error for an invalid email", () => {
  render(<SignupForm />);

  const emailInput = screen.getByLabelText("Email");
  fireEvent.change(emailInput, { target: { value: "not-an-email" } });
  fireEvent.click(screen.getByText("Sign Up"));

  expect(screen.getByText(/valid email/i)).toBeInTheDocument();
});

This directly tests Chapter 15's validation logic: simulate typing invalid text with fireEvent.change, submit the form, and assert the expected error message actually appears โ€” exercising the real validation function through the UI, exactly as a user would trigger it.

3Testing Loading and Error States
test("shows a loading message before data arrives", () => {
  global.fetch = vi.fn(() => new Promise(() => {}));  // a Promise that never resolves
  render(<Users />);
  expect(screen.getByText("Loading...")).toBeInTheDocument();
});

test("shows an error message when the fetch fails", async () => {
  global.fetch = vi.fn(() => Promise.reject(new Error("Network error")));
  render(<Users />);
  await waitFor(() => {
    expect(screen.getByText(/error/i)).toBeInTheDocument();
  });
});

A never-resolving Promise is a simple, reliable trick to freeze a component in its loading state for testing. Rejecting the mocked fetch simulates a failed request, letting you verify Chapter 21's error-state handling actually works, without needing a real API to intentionally fail on command.

4A Small Testing Project Checklist
  • A button component: verify its click handler fires correctly
  • A counter: verify clicking updates the displayed count
  • A form: verify submitting valid data calls the expected handler
  • Form validation: verify invalid input shows the correct error message
  • An API-driven list: verify loading, error, and success states each render correctly

Working through this checklist against a real project of your own is one of the fastest ways to genuinely internalize the testing patterns from Chapter 34.

โš ๏ธ Writing One Giant Test That Checks Everything at Once

A single sprawling test that renders a whole page and checks a dozen unrelated things is hard to debug when it fails โ€” you won't immediately know which specific behavior broke. Keep each test focused on one specific behavior (as in every example above), so a failing test's name immediately tells you what's actually wrong.

๐Ÿ’ป Hands-on Interactive Practice Challenge

Write a test for a simple toggle button component that verifies its displayed text changes from "Show" to "Hide" after being clicked.

React Practice Challenge โ–ถ Run in Compiler
import { render, screen, fireEvent } from "@testing-library/react";
import { test, expect } from "vitest";
import { useState } from "react";

function ToggleButton() {
  const [visible, setVisible] = useState(false);
  return (
    <button onClick={() => setVisible(!visible)}>
      {visible ? "Hide" : "Show"}
    </button>
  );
}

test("toggles text between Show and Hide", () => {
  render(<ToggleButton />);
  const button = screen.getByText("Show");
  fireEvent.click(button);
  expect(screen.getByText("Hide")).toBeInTheDocument();
});
Run This Challenge in Online JS IDE โ†’
โ“ Frequently Asked Questions (FAQ)

Q What does vi.fn() actually do?

It creates a 'mock' function โ€” a fake function that records how many times it was called and with what arguments, without needing real implementation logic. This lets a test verify a callback prop fired correctly without depending on what that callback actually does.

Q Why simulate a never-resolving Promise to test loading state?

Because a real fetch resolves too quickly to reliably 'catch' the component in its loading state during a test. A Promise that never resolves guarantees the component stays in loading mode for the entire duration of that specific test, making the loading UI reliably testable.

OC
Written by Our Compiler Technical Editorial Team
Reviewed for accuracy & tested on React 18+ ยท Last updated August 2026