If you're still using Enzyme for React testing in 2026, it's time to switch. Testing Library has become the standard — and here's why.
Why Testing Library Won
Testing Library tests your components the way users actually interact with them. No shallow rendering, no implementation details, no brittle tests that break on refactors.
Quick Start
bun add -d @testing-library/react @testing-library/jest-dom @testing-library/user-event
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { LoginForm } from "./LoginForm";
test("submits login form", async () => {
const onSubmit = vi.fn();
render(<LoginForm onSubmit={onSubmit} />);
await userEvent.type(screen.getByLabelText("Email"), "alice@example.com");
await userEvent.type(screen.getByLabelText("Password"), "secret123");
await userEvent.click(screen.getByRole("button", { name: "Sign In" }));
expect(onSubmit).toHaveBeenCalledWith({
email: "alice@example.com",
password: "secret123",
});
});
Core Queries (Pick the Right One)
Priority Order (use top ones first):
- getByRole — best for accessibility
screen.getByRole("button", { name: "Submit" });
screen.getByRole("heading", { level: 2 });
screen.getByRole("textbox", { name: "Email" });
- getByLabelText — for form fields
screen.getByLabelText("Password");
- getByPlaceholderText — fallback for forms
screen.getByPlaceholderText("Search...");
- getByText — for non-interactive elements
screen.getByText("Welcome back!");
screen.getByText(/total: \$\d+/i); // regex works too
- getByTestId — last resort
screen.getByTestId("custom-element");
Testing Async Behavior
test("loads user data", async () => {
render(<UserProfile userId="123" />);
// Wait for loading to finish
expect(screen.getByText("Loading...")).toBeInTheDocument();
// findBy* waits automatically (up to 1000ms)
const userName = await screen.findByText("Alice Johnson");
expect(userName).toBeInTheDocument();
// Verify loading is gone
expect(screen.queryByText("Loading...")).not.toBeInTheDocument();
});
userEvent vs fireEvent
// BAD: fireEvent dispatches a single DOM event
fireEvent.click(button); // Just one click event
// GOOD: userEvent simulates real user behavior
await userEvent.click(button); // focus → pointerdown → mousedown → pointerup → mouseup → click
await userEvent.type(input, "hello"); // focus → keydown → keypress → input → keyup (per char)
Always prefer userEvent — it catches more bugs because it simulates real browser behavior.
Common Patterns
Testing Modals
test("opens and closes modal", async () => {
render(<App />);
await userEvent.click(screen.getByText("Open Settings"));
expect(screen.getByRole("dialog")).toBeInTheDocument();
await userEvent.click(screen.getByRole("button", { name: "Close" }));
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
});
Testing API Calls
test("displays fetched data", async () => {
server.use(
http.get("/api/users", () => {
return HttpResponse.json([{ id: 1, name: "Alice" }]);
})
);
render(<UserList />);
const user = await screen.findByText("Alice");
expect(user).toBeInTheDocument();
});
Testing Error States
test("shows error on failed fetch", async () => {
server.use(
http.get("/api/users", () => {
return new HttpResponse(null, { status: 500 });
})
);
render(<UserList />);
const error = await screen.findByRole("alert");
expect(error).toHaveTextContent("Failed to load users");
});
Migration from Enzyme Cheat Sheet
| Enzyme | Testing Library |
|---|---|
shallow() |
render() (no shallow!) |
wrapper.find(".class") |
screen.getByRole() |
wrapper.instance() |
Not needed (test behavior) |
wrapper.state() |
Not needed (test output) |
wrapper.setProps() |
rerender(<Comp newProp />) |
wrapper.simulate("click") |
await userEvent.click() |
Need to test web scraping output? Check out my ready-to-use scraping actors on Apify Store — validated data extraction without writing code. For custom solutions, email spinov001@gmail.com.
Top comments (0)