Testing Library's guiding principle is printed at the top of its own documentation: the more your tests resemble the way your software is used, the more confidence they can give you. It sounds like a slogan, but it's a genuinely load-bearing constraint on the API's design — there is no way to select a DOM node by component name, internal state, or CSS class through Testing Library's primary query methods, because all three are things a user cannot see.

Why There's No shallow Render

Enzyme's shallow rendering let you test a component in isolation, stubbing out its children entirely. That isolation is exactly what Testing Library refuses to provide, on purpose. A shallow-rendered component can pass its test while being completely broken when a real child component is substituted in — the test verified an implementation detail (this component renders <ChildComponent />) rather than a behavior (this component displays the child's actual content correctly).

jsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

test('adds an item to the cart', async () => {
  const user = userEvent.setup();
  render(<ProductPage product={mockProduct} />);

  await user.click(screen.getByRole('button', { name: /add to cart/i }));

  expect(screen.getByText(/1 item in cart/i)).toBeInTheDocument();
});

This test renders the full component tree — ProductPage, its AddToCartButton, its CartBadge — and interacts with it the way a real user would: finding a button by its accessible name and clicking it. If any part of that chain breaks, the test fails, which is exactly the coverage you want.

The Query Priority Is Itself Accessibility Guidance

Testing Library's queries are explicitly ranked, and the ranking isn't arbitrary — it mirrors how assistive technology finds things on a page:

  1. getByRole — the top choice, because it's how screen reader users navigate.
  2. getByLabelText — for form fields, matching how a label associates with its input.
  3. getByText — for non-interactive content.
  4. getByTestId — the escape hatch, used only when nothing semantic is available.

A codebase full of getByTestId calls isn't just a testing smell — it's usually a sign the markup itself lacks the roles and labels a screen reader would need, since the same query gap exists for both. Reaching for getByRole('button', { name: 'Submit' }) and having it fail because the element is a <div onClick> instead of a <button> is Testing Library surfacing an accessibility bug as a test failure, which is the intended side effect, not a coincidence.

userEvent Over fireEvent

fireEvent.click dispatches exactly one DOM event. Real clicking involves a sequence — pointerdown, mousedown, focus, pointerup, mouseup, click — and typing involves individual keydown/keyup/input events per character, not one bulk value assignment:

jsx
// Fires one event, skips everything real typing implies
fireEvent.change(input, { target: { value: 'hello' } });

// Simulates the actual sequence of events a keyboard produces
await userEvent.type(input, 'hello');

The difference matters for any component with logic tied to intermediate states — a search box's debounce, a masked input's per-keystroke formatting, focus-triggered validation. fireEvent will miss bugs in that logic that userEvent catches because it's the only one that actually exercises it.

What This Philosophy Deliberately Sacrifices

Testing Library tests are, by design, harder to pin down to a single cause of failure than a narrow unit test of one function — a broken ProductPage test could mean the button's label changed, the cart logic broke, or a child component threw. That's an accepted trade for the confidence gained: a passing test means the feature actually works end to end from a user's perspective, not that an isolated unit behaves correctly in an assembly nobody verified. For genuinely isolated logic — a pricing calculation, a date formatter — a plain unit test without any rendering is still the right, faster tool; Testing Library's philosophy applies to components, not to every function in the codebase.