Test User Behavior

Write tests at the behavior level using React Testing Library; avoid shallow rendering.

Why?  Tests reflect real user interactions, not internal implementation.

Behavior-driven tests exercise your components the same way real users do—through the DOM and events—so refactors that keep UI contract stable don’t break the suite. Shallow tests poke at implementation details (state fields, method calls, snapshot structure) and create brittle noise that slows iteration.

Correct Example

// DarkModeToggle.test.tsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { DarkModeToggle } from "./DarkModeToggle";

test("toggles label and aria-pressed on click", async () => {
  render(<DarkModeToggle />);

  const btn = screen.getByRole("button", { name: /toggle dark mode/i });
  expect(btn).toHaveAttribute("aria-pressed", "false");
  expect(btn).toHaveTextContent("Off");

  await userEvent.click(btn);

  expect(btn).toHaveAttribute("aria-pressed", "true");
  expect(btn).toHaveTextContent("On");
});

Why it’s good:

  • Uses public queries (getByRole) and real clicks via userEvent.
  • Asserts observable output—text and ARIA state—rather than internals.
  • Works regardless of hook / state implementation changes.

Incorrect Example

// DarkModeToggle.shallow.test.tsx
import { shallow } from "enzyme";
import { DarkModeToggle } from "./DarkModeToggle";

test("toggles internal state", () => {
  const wrapper = shallow(<DarkModeToggle />);
  const instance: any = wrapper.instance(); // class-only trick

  expect(instance.state.on).toBe(false);
  wrapper.find("div").simulate("click");
  expect(instance.state.on).toBe(true); // ❌ tightly coupled
});

What’s wrong:

  • Relies on Enzyme shallow rendering (discouraged, no React 18 support).
  • Peeks at private state, breaking when you convert to Hooks.
  • Click simulated on <div>; if you swap to <button> the test fails even though UX stays identical.

Key Takeaways

  • Favor React Testing Library + user-event for realistic interaction.
  • Assert visible DOM and ARIA changes, not component internals or snapshots.
  • One failing test should signal real user-visible regressions, not harmless refactors.
  • Shallow rendering is obsolete under React 18 concurrent & RSC paradigms.
  • Keep tests colocated with features; they document expected behavior for new contributors.