Most component tests break when you refactor. Testing Library tests survive because they test behavior, not implementation.
The Problem with Traditional Testing
// Enzyme style — tests implementation details
const wrapper = shallow(<Counter />);
expect(wrapper.state("count")).toBe(0);
wrapper.find(".increment-btn").simulate("click");
expect(wrapper.state("count")).toBe(1);
Rename the CSS class? Test breaks. Change state shape? Test breaks. Switch to hooks? Test breaks. But the component still works.
Testing Library Approach
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
test("increments counter when button is clicked", async () => {
render(<Counter />);
expect(screen.getByText("Count: 0")).toBeInTheDocument();
await userEvent.click(screen.getByRole("button", { name: /increment/i }));
expect(screen.getByText("Count: 1")).toBeInTheDocument();
});
Rename CSS classes? Still passes. Refactor to hooks? Still passes. Change state management? Still passes. Because we test what the user sees and does.
Core Queries (Priority Order)
// 1. Accessible by everyone
screen.getByRole("button", { name: /submit/i }); // BEST — semantic
screen.getByLabelText("Email"); // Form inputs
screen.getByPlaceholderText("Search..."); // Fallback
screen.getByText("Welcome back!"); // Visible text
screen.getByDisplayValue("current input value"); // Form values
// 2. Semantic HTML queries
screen.getByAltText("Profile photo"); // Images
screen.getByTitle("Close"); // Titles
// 3. Last resort (still better than class/id)
screen.getByTestId("custom-element"); // data-testid
Async Queries
// Wait for element to appear
const message = await screen.findByText("Data loaded!");
// Assert element is NOT present
await waitForElementToBeRemoved(() => screen.queryByText("Loading..."));
// Wait for condition
await waitFor(() => {
expect(screen.getByText("3 results")).toBeInTheDocument();
});
User Events (Realistic Interactions)
import userEvent from "@testing-library/user-event";
const user = userEvent.setup();
// Type in input
await user.type(screen.getByLabelText("Email"), "user@test.com");
// Clear and type
await user.clear(screen.getByLabelText("Name"));
await user.type(screen.getByLabelText("Name"), "New Name");
// Select dropdown
await user.selectOptions(screen.getByLabelText("Country"), "US");
// Upload file
const file = new File(["hello"], "test.png", { type: "image/png" });
await user.upload(screen.getByLabelText("Avatar"), file);
// Keyboard
await user.keyboard("{Enter}");
await user.tab();
Testing Patterns
Form Submission
test("submits form with valid data", async () => {
const onSubmit = vi.fn();
render(<ContactForm onSubmit={onSubmit} />);
await user.type(screen.getByLabelText("Name"), "Alice");
await user.type(screen.getByLabelText("Email"), "alice@test.com");
await user.click(screen.getByRole("button", { name: /send/i }));
expect(onSubmit).toHaveBeenCalledWith({
name: "Alice",
email: "alice@test.com",
});
});
Error States
test("shows validation errors", async () => {
render(<ContactForm />);
await user.click(screen.getByRole("button", { name: /send/i }));
expect(screen.getByText("Name is required")).toBeInTheDocument();
expect(screen.getByText("Email is required")).toBeInTheDocument();
});
Works Everywhere
-
@testing-library/react— React -
@testing-library/vue— Vue -
@testing-library/svelte— Svelte -
@testing-library/angular— Angular -
@testing-library/preact— Preact
Same API, same philosophy, every framework.
Need robust testing for your web applications? I build developer tools and data pipelines. Email spinov001@gmail.com or check my Apify tools.
Top comments (0)