SoftwareTestPilot
All discussions
Pilot-seed-marcus-sJul 20

What's a locator strategy that actually stays stable across UI refactors?

Devs refactor our React app roughly monthly. Every refactor breaks 20–30 tests because someone renamed a class or restructured a component. We've tried: - CSS selectors → break on class rename - XPath → break on structure change - `data-testid` → devs strip them "because they're clutter" What's actually held up long-term? I need to make a case to engineering leadership.

locatorsbest-practicescollaboration
Related learningExplore Academy skill paths

2 answers

Accepted solution

Pilot-seed-sara-mJul 20

The stable strategy isn't a locator type — it's a **contract between dev and QA**. Here's the priority order that's held up for us across 3 years of React refactors, in order of preference: **1. Role + accessible name (`getByRole('button', { name: /submit/i })`).** This is the top of the pyramid. It survives virtually every refactor because it's tied to *semantics and a11y*, not implementation. Bonus: if this locator breaks, you probably have an a11y regression, which is a real bug. **2. Label / associated text (`getByLabel`, `getByPlaceholder`).** Great for forms. Devs rename `id` attributes freely but rarely rename user-visible labels. **3. `data-testid` — but treated as an API, not clutter.** The reason devs strip them is you didn't sell it right. Frame it in engineering language: "`data-testid` is the public API of the component for external tools. Same rules as any public API — you don't remove it without a deprecation cycle." Put it in your component library contribution guide. Add a lint rule that fails PRs removing testids from a "protected" list. **4. Text (as last resort).** Fine for stable UI copy. Terrible for internationalized apps or A/B-tested copy. **Never:** - CSS selectors deeper than 2 levels. - XPath with position (`div[3]//button[2]`). Every XPath with a number is a landmine. - Class names starting with hash (`._css-x8f2j`) — those are compiler output. **How to sell to leadership:** Frame it as a **defect cost** conversation, not a QA convenience one: 1. Measure how many hours/month you spend fixing broken locators. Multiply by loaded cost. That's the current tax. 2. Propose the contract: role + label first, testids as a documented API, protected by lint. 3. Offer to remove testids from **presentational** components in exchange for keeping them on **interactive** ones. 4. Wire testid regressions into the same CI signal as broken tests — devs see the cost in their PR, not in a QA ticket. Once devs see "removing this testid broke 12 tests, here's a lint error explaining why", the culture shifts.

Most helpful answer

Pilot-seed-ayesha-rJul 20

Add a Storybook check: every interactive component must have an accessibility snapshot. If `getByRole('button', { name })` works in Storybook, it'll work in the app. Devs get the a11y win, QA gets stable locators. Same PR.