Should I test that?

Should I use data-testid attributes in tests?

Verdict

No

No, do not find buttons, links and form fields by data-testid; find them by role and accessible name, and add a data-testid only to an element that has no role or name to query, such as a chart drawn on a canvas.

Why

Do not find labelled controls by data-testid; find them by role and accessible name. My typical case is a customer-facing web app whose tests drive buttons and fields with a visible label. Blast radius is users and Change frequency is regularly, because these controls reach customers and change about once a month. Detectability is immediately: a role query fails in CI when the button goes, so a test ID catches nothing more. Reversibility is trivial, because a markup revert restores the element, and Test cost is moderate, because each ID is one more attribute to keep in sync. Rule R8 gives Do not test by test ID.

When the decision changes
WhenDecisionWhy
The element has no role or name to query, such as a chart that a third-party library draws on a canvasTest minimally: put a data-testid on the chart's wrapper and assert in one test that the chart renders for sample dataDetectability moves to same-day, because no role query reaches the chart and only a user report shows it blank
An older screen has icon-only div controls with click handlers and no role or visible textTest minimally: add a data-testid to each icon-only control of the main flow until the div becomes a labelled buttonDetectability moves to same-day, because no query reaches these controls and only a user report shows a dead one
An icon-only button, such as Share, has its name only in aria-label and no flow test clicks itTest minimally: add no data-testid; assert each such button's name with toHaveAccessibleName or the axe button-name ruleDetectability moves to eventually, because no test touches the button and sighted developers see only the icon
The control is the Pay button on the checkout pageTest mandatory: a checkout test on every change that finds Pay by role and nameBlast radius rises to money and Reversibility to costly, because a customer who cannot pay buys elsewhere
Every control already carries a data-cy attribute, so a test that uses one costs nothing extraDo not use the data-cy attributes in new tests; write role queries and convert old tests when they breakTest cost falls to trivial, but Detectability stays immediately and Reversibility trivial, so the decision holds

What breaks if you don't test

Nothing breaks when a labelled button has no test ID, because the role query fails when the button disappears. The failures that slip through come from test ID queries, which still pass after a label stops pointing to its input or a button becomes a div; screen reader users meet the break weeks later.

What you lose if you over-test

A component copied to a second screen brings its ID along, and Playwright locators are strict, so the test fails on two matches until someone renames one. The bigger loss is false confidence: the suite stays green while a control has lost its name. Match short names such as /save/i, not whole sentences that fail on every moved comma.

What to do instead

  1. Find controls with getByRole and a name, and fields with getByLabel. The Testing Library query priority lists getByTestId last, for cases where role or text cannot match.
  2. For an Edit button in every table row, find the row first: page.getByRole('row').filter({ hasText: 'Order 1042' }).getByRole('button', { name: 'Edit' }).
  3. Use a test ID only for elements with no role or name: Playwright's getByTestId reads data-testid, and testIdAttribute switches it to data-cy.
  4. In Cypress, add Cypress Testing Library for role queries instead of the data-cy selectors in the Cypress guide.

When the answer changes

  • The element has no role and no name: a canvas, a map, or an icon-only div control with no text.
  • An icon-only button that no test clicks is named only in aria-label.
  • The control takes money, as Pay does.

Real incident + Code example

The passenger form that said "edit text" eight times

On a travel booking product I worked on, a refactor moved the passenger form's input IDs to React's useId, but the labels kept their old hard-coded htmlFor values. All eight fields lost their accessible names and sounded the same to a screen reader. Our 94 Cypress tests found the fields by data-cy and passed. Seventeen days later a screen reader user wrote to support that the form announced "edit text" eight times. We moved the form tests to label queries:

// Before: passes whether or not a label names the input
cy.get('[data-cy="first-name"]').type('Ana');

// After: fails when no label is linked to the input
cy.findByLabelText('First name').type('Ana');
cy.findByRole('button', { name: 'Continue' }).click();

// A test ID stays where no role or name exists: the route map canvas.
// The map sets data-markers after drawing, so a blank canvas fails here
cy.get('[data-cy="route-map"]').should('have.attr', 'data-markers', '2');

FAQ

Should I add data-testid attributes for UI tests?

Add data-testid only to elements that tests cannot find by role, label or text, such as a chart drawn on a canvas. For buttons, links and form fields, query by role and accessible name.

Is getByRole better than getByTestId?

getByRole catches more bugs than getByTestId for any element that has a role, because it fails when the element loses its role or its accessible name. Use getByTestId only for an element with no role or name, such as a canvas chart.

Why does Cypress recommend data-cy attributes?

The Cypress guide recommends data-cy attributes because they stay the same when text, CSS classes or markup change. A data-cy test therefore passes after a control loses its label, so query by role wherever the element has one.

Should I remove data-testid attributes in production builds?

Keep data-testid attributes in production builds when your end-to-end tests run against the production build, because removing them breaks those tests. When every test runs against a development or staging build, keep the data-testid attributes as well, because one such as data-testid="first-name" is 24 bytes of HTML. Strip them with the babel-plugin-react-remove-properties build plugin only when a page size budget requires it.