data-testid gets a bad reputation.
Some people say it’s lazy. Others say it’s an anti-pattern.
Both are wrong.
data-testid is a tool. Like any tool, it’s either used with intent or abused.
Let’s be honest about when it’s the right choice and when it’s not.
What data-testid actually is
data-testid is a testing-only attribute added to the DOM.
Example:
<button data-testid="save-btn">Save</button>
Used in Playwright:
page.getByTestId('save-btn')
That’s it. Nothing magical.
Why people hate data-testid
The usual arguments:
- “Tests should use what users see”
- “It couples tests to implementation”
- “It pollutes the markup”
These complaints come from bad usage, not the attribute itself.
The real problem: unstable selectors
Most flaky tests don’t fail because of Playwright.
They fail because selectors are tied to:
- DOM structure
- CSS classes
- Layout
- Styling refactors
Example of a bad selector:
page.locator('div > button.primary > span').click()
This breaks when anything visual changes.
Where data-testid shines
Use data-testid when:
1. There are multiple identical elements
Example:
- Multiple “Save” buttons
- Same label, different contexts
page.getByTestId('profile-save-btn')
Clear. Unambiguous.
2. Accessibility selectors are not possible
Not every app is perfectly accessible.
If you can’t rely on:
getByRole
getByLabel
data-testid is better than brittle DOM hacks.
3. The element is not user-facing
Icons, loaders, skeletons, hidden triggers.
<div data-testid="loading-spinner" />
Users don’t interact with these. Tests still need to.
When you should NOT use data-testid
Do not use it when:
❌ A semantic selector works
If this works:
page.getByRole('button', { name: 'Save' })
Use it.
It’s closer to user behavior.
❌ You’re replacing bad DOM design
If everything needs data-testid, that’s a UI problem.
data-testid is not a substitute for:
- proper labels
- roles
- accessibility
data-testid vs accessibility selectors
This is the honest hierarchy:
- getByRole / getByLabel
- getByTestId
- Text selectors (carefully)
- CSS / XPath (last resort)
data-testid is not last.
It’s second.
Naming data-testid properly (important)
Bad:
data-testid="button1"
data-testid="save"
Good:
data-testid="profile-save-btn"
data-testid="article-publish-button"
Rules:
- Describe intent
- Be stable
- Don’t encode layout or position
Why strict mode makes data-testid valuable
Playwright strict mode fails fast:
strict mode violation: locator resolved to 2 elements
This is good.
data-testid gives you guaranteed uniqueness without hacks like .first().
The real takeaway
data-testid is not cheating.
It’s declaring a testing contract.
You’re saying:
“This element matters for tests. Don’t accidentally break it.”
That’s responsible engineering.
Final opinion
If your test breaks when the UI still works, the selector is wrong.
Sometimes the cleanest fix is:
page.getByTestId('save-btn')
No shame in that.

Write tests for behavior.
Use selectors that survive change.