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.
// 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:
getByRole
) and real clicks via userEvent
.// 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:
<div>
; if you swap to <button>
the test fails even though UX stays identical.