skip to content

React Testing Library documents a recommended priority order for its queries. What is that order, and why does getByRole sit at the top while getByTestId sits at the bottom?

level: juniorimportance: must knowfreq 80%

answer

  1. query the way the user finds it
  2. three tiers, not a flat list
  3. role plus accessible name first
  4. test id is the escape hatch
  5. the query audits your markup

basics

~10 s

Queries are ranked by how closely they mirror how a real person finds a control: getByRole with an accessible name first, then getByLabelText, getByPlaceholderText, getByText and getByDisplayValue, then getByAltText and getByTitle, and getByTestId last.

solid answer

~40 s

Testing Library groups its queries into three tiers. The top tier is what everyone perceives: `getByRole` with a `name` option covers almost every interactive control, then `getByLabelText` for form fields, then `getByPlaceholderText`, `getByText` and `getByDisplayValue`. The middle tier is semantic but not universally perceived — `getByAltText` and `getByTitle`. The bottom tier is `getByTestId`, an escape hatch for elements a user has no way to identify, like a chart canvas or a container with no text. The order is not stylistic. A role-plus-name query depends only on what the component renders to the user, so it survives class renames, wrapper divs and CSS-in-JS churn. It also fails loudly when markup is not exposed to assistive technology, so picking the query is itself a cheap accessibility check.

code

javascript · 16 lines
javascript
import { screen } from '@testing-library/dom'

document.body.innerHTML = `
  <form>
    <label for="email">Email address</label>
    <input id="email" type="email" />
    <button type="submit" class="btn btn--primary">Sign up</button>
  </form>
`

// tier 1: survives class and wrapper changes
screen.getByRole('button', { name: 'Sign up' })
screen.getByLabelText('Email address')

// bottom of the ladder: only when nothing user-facing identifies the element
// screen.getByTestId('signup-submit')

go deeper

for a junior

Be able to recite the order — role, label, placeholder, text, display value, then alt/title, then test id — and say in one sentence that it mirrors how a user finds things.

for a middle

Explain the mechanics: a role query resolves against the accessibility tree, so it ignores classes and wrapper elements, which is exactly why it survives a refactor that breaks a CSS selector.

for a senior

Show judgment about when the ladder is being used as a fig leaf: a codebase drowning in data-testid usually has controls that are not exposed to assistive technology, and you should treat a failing role query as a product bug before touching the test.

for a principal

Own the tradeoff at team scale — where a test-id convention is legitimately allowed, how the query policy is enforced in review or lint, and the cost of a suite whose tests pass while the UI is unreachable by keyboard.

## The ladder Testing Library ships many queries and deliberately ranks them, because the query you choose decides what your test is actually coupled to. **Tier 1 — accessible to everyone.** These find elements the way any user does. - `getByRole(role, { name })` — the workhorse. Every interactive element has a role (`button`, `link`, `textbox`, `checkbox`, `heading`, `dialog`, `row`…), and the `name` option matches the element's accessible name. `getByRole('heading', { level: 2, name: /billing/i })` targets one specific heading. - `getByLabelText` — a form field found by its label, which is exactly how a sighted user finds it. - `getByPlaceholderText` — a weaker fallback for fields. - `getByText` — non-interactive content: paragraphs, headings, status messages. - `getByDisplayValue` — a filled-in field found by its current value. **Tier 2 — semantic queries.** `getByAltText` for images and `getByTitle` for elements whose only handle is a `title` attribute. These depend on attributes that not every user or every technology surfaces, so they sit below tier 1. **Tier 3 — test IDs.** `getByTestId` matches an attribute (`data-testid` by default) that exists only for tests. ## Why role sits at the top A role query resolves against the accessibility tree the browser builds from your markup. That has two consequences. First, **refactor resilience**. Consider a submit button. `container.querySelector('.btn-primary')` breaks when the class is renamed. `container.querySelector('form > div > button')` breaks when someone adds a wrapper. `getByRole('button', { name: 'Sign up' })` keeps working through all of that, because it only cares that a button labelled "Sign up" exists. The test asserts the contract the user relies on, not the internal structure the team is free to change. Second, **the query doubles as an audit**. If `getByRole('button', { name: 'Sign up' })` cannot find your control, it is usually because the control is a `<div onClick>` with no role, or has no text a machine can read. That is a real defect for keyboard and screen-reader users; the test just found it for free. Dropping to a test ID at that moment hides the defect instead of fixing it. ```javascript // brittle: coupled to markup structure container.querySelector('form .actions button.primary') // durable: coupled to what the user sees screen.getByRole('button', { name: 'Sign up' }) ``` ## The middle rungs `getByLabelText` is the right first choice for a form field, because a field's label is its user-facing identity and the query fails when the label is not wired up. `getByPlaceholderText` works but is weaker — placeholders vanish as soon as the user types. `getByText` is for content, not controls: use it to assert an error message appeared, not to click a button (a button found by its text is better expressed as a role query with a `name`). ## When a test ID is the right answer The ladder is a priority order, not a ban. A test ID is legitimate when nothing user-perceivable identifies the element: a `<canvas>` chart, a layout region with no heading, a third-party widget you do not control, or a repeated card where the accessible content is genuinely identical. The rule of thumb is that you reach for `getByTestId` after you have decided the element *should not* have a user-facing handle — not because writing a role query took an extra minute. ```javascript // legitimate: nothing about a canvas is user-queryable screen.getByTestId('revenue-chart') ``` ## What the ladder buys you Teams that hold the line on tier 1 end up with tests that read like a description of the product ("click the button named Save, then the alert says Saved") and that survive redesigns. Teams that default to test IDs end up with tests that pass while the UI is unusable, because a `data-testid` is present in exactly the markup a screen reader cannot use. The order encodes that tradeoff, and it is why interviewers ask for it: reciting the list is easy, explaining *why* role beats test ID is the actual question.

  • Where do getByAltText and getByTitle sit, and why are they not on the top rung?
    They are the middle tier — semantic, but not perceived by everyone. Alt text is only surfaced to assistive technology and to users with images off, and a `title` attribute is invisible to touch users and inconsistently announced. They are better than a test ID because they still describe user-facing meaning, but weaker than a role or label query.
  • How would you query a control that genuinely has no accessible name and no visible text?
    First treat it as a bug: an interactive control with no name is unusable by screen-reader and voice-control users, so the fix is usually in the component, not the test. If it is genuinely not a control — a canvas, a decorative region, a vendor widget — then a `data-testid` is the correct, honest choice, and the test id documents that the element has no user-facing handle.
  • Two buttons on the page are both labelled "Edit". Does that mean you must fall back to a test ID?
    No. Scope the query instead: find the enclosing region or row and run the role query inside it with `within`. If the labels really are ambiguous to a user as well, that is a product problem — a screen-reader user hears "Edit, Edit" too — and the fix is a more specific accessible name rather than a test-only attribute.

saying these in an interview costs you the question

  • Says test ids are fine everywhere because they are stable
  • Thinks getByRole matches a CSS class or tag name
  • Treats the ladder as taste, with no refactor argument
  • Reaches for container.querySelector whenever a query is awkward
  • Claims role queries only matter to screen-reader users

context