Resilient UI Tests: Stable Selectors That Survive Changes

CSS selectors are the most common way to find elements in end-to-end tests, besides xpath. They’re also fragile. Everything works one day, and the next a CSS class gets renamed. Styles change, the business flow doesn’t, and the test fails anyway. Now you have to maintain it.

Developers could fix those tests for us, but they don’t know our scenarios, and they can’t always tell whether a refactor touches a test. I think there’s a simple fix.

Why are CSS selectors a problem in UI tests?

Imagine you had to write an ugly selector. The element was deeply nested, and the only option was to rely on CSS classes and HTML structure. Then a developer refactors a bit and removes some useless HTML tags. Your selector used them. The business flow is unchanged, and the test fails.

CSS classes exist to style elements, not to serve end-to-end tests. Class names change all the time without any effect on the business flow, which is the part end-to-end tests are supposed to protect. It hurts to maintain a test when no user flow changed.

Both problems have the same fix: find a better way to get elements.

What is the most stable selector for a UI test?

In order of how well they survive a refactor:

Selector Example Survives
Role and accessible name getByRole('button', { name: 'Sign in' }) Markup and class changes. Breaks if the label text changes.
Test id attribute getByTestId('login-submit') Markup, class and text changes.
Visible text or label getByLabel('Password') Markup and class changes. Breaks on copy edits.
CSS class .btn-primary Almost nothing. Classes exist for styling.
Structure path (XPath, div > div > span) //div[3]/span Nothing.

Start with role-based locators where the element has a clear role and name. Playwright’s best practices recommend testing what the user sees and interacts with, not implementation details such as CSS classes. When the element has no good role or the text changes often (prices, generated names, translations), use a test id. That is where the rest of this post applies.

Use test id attributes

They’re controversial, but they have two real advantages. I wrote this post with data-test-id in mind. Today the common default is data-testid: Playwright’s getByTestId() locates elements by that attribute and lets you change the attribute name in the config, so use whatever your team prefers and stick to it.

First, they survive refactors. If an element has the attribute, it doesn’t matter whether the developer changes the HTML structure or the CSS classes. We still find it. Unless the element is removed or the business flow changes, the test works.

Second, the developer sees the attribute in the code. That’s a signal: they can warn us about a change, or update the test themselves, and the attribute makes it easy to grep for the tests that use an element. With more people working on end-to-end tests, the tests get more reliable.

Should I add a test id to every element?

No. Only to elements the tests use. If nearly every element has the attribute and only a few are tested, the code turns into a mess, and nobody trusts the attribute. We want developers to see that their change could break our work, so the attribute has to mean something.

It’s extra code. If you don’t want the attributes in production, strip them in the build step. That is optional, since they are harmless markup.

What are the benefits of test ids?

It gets us talking to developers. If they want to refactor an element that has the attribute, they either update the test (best case) or tell us something might break. Knowing that before the tests run on the testing environment is useful. Tests stop failing for changes that don’t affect the business flow, and people start trusting them.

All we have to do is give developers a reason and a way to maintain the tests.

Conclusion

Code structure changes, and it shouldn’t break tests unless the business flow changes. CSS-based selectors break them. That’s why I reach for test ids when a role locator isn’t enough.

It has shortcomings, but I think the pros win. You’ll work closer with developers, which can lead to other useful ideas. Just add the attribute only to the elements your tests use.

Stable selectors are one piece of a maintainable suite. The next piece is how you organize them: see six things to consider when designing a test architecture, which covers the Page Object Model. If you are setting up your first browser tests, the Playwright tutorial uses role-based locators from the start. And if you wonder where this skill sits in a QA role, read who a QA engineer is and what they should know.

Have a nice day!