Designing Reliable UI Tests
UI tests have a reputation for being brittle. They fail when a button moves two pixels. They timeout when the network is slow. They take forever to run. But it doesn't have to be this way. Here are principles for designing UI tests that earn their keep.
1. Test Behavior, Not Implementation
Don't couple your tests to CSS classes, DOM structure, or implementation details. Couple them to user-visible behavior.
// Fragile: breaks when class names change
await page.click('.btn-primary.submit-form');
// Better: use role, label, or test ID that reflects intent
await page.click('button[type="submit"]');
// or: getByRole('button', { name: 'Submit' })
If the user doesn't care about a class name, your test shouldn't either.
2. Use Stable Selectors
Prefer semantic attributes over layout:
roleandaria-labelfor accessibility-minded selectorsdata-testidwhen you need a hook and nothing else fits- Avoid: nth-child, generic class names, long XPath chains
When you add a data-testid, you're making a contract: "This element is important for testing." That's okay. Just keep the IDs meaningful and stable.
3. Wait for Real Conditions
Don't rely on fixed sleep() calls. Wait for the state you actually care about:
- Element visible and stable
- Network request completed
- Animation finished
Most test frameworks provide waitFor-style APIs. Use them. A test that sleeps for 3 seconds is either slow or still flaky.
4. Isolate Tests
Each test should be independent. Don't assume a previous test left the system in a known state. Use fixtures, setup/teardown, or a fresh session. Shared state is the enemy of reliable tests.
5. Keep the Feedback Loop Tight
If your UI suite takes 30 minutes, people will stop running it. Prioritize:
- A small smoke suite that runs in minutes
- Parallelization for the full suite
- Clear failure messages so you know what broke, not just that it broke
Summary
Reliable UI tests are possible. They require discipline: stable selectors, behavioral focus, proper waiting, isolation, and a fast feedback loop. Invest in these, and your UI tests will start to feel like assets instead of liabilities.