Imagine this scenario: A development team races to launch a new feature, only to discover that half their automated tests are failing because an unexpected style change broke key selectors. QA scrambles to update the locators, while the product manager worries about delivery deadlines. Suddenly, what should have been a quick UI tweak balloons into a multi-day ordeal. Sound familiar?
This story is more common than you might think—but it’s avoidable. By building websites with testability in mind from the very start, you can streamline development, reduce flakiness in your tests, and ship features with greater confidence. The best part? It doesn’t take much extra work—just a few strategic decisions that pay off for your entire organization.
Below, we’ll cover ten practical tips for making your website fully testable and resilient to change. You’ll see how these small details can yield big results for Developers, QA engineers, and Product Managers alike.
1. Embrace Test-Friendly Locators
Common Pitfall
Teams often rely on dynamic IDs or CSS classes for identifying elements (e.g., .btn-primary
)—until a designer changes the color scheme and all .btn-primary
references vanish. That’s a recipe for flaky tests.
What to Do Instead
- Use
data-test
attributes for key elements:
<!-- Before -->
<button class="btn-primary">Sign In</button>
<!-- After -->
<button data-test="login-button">Sign In</button>
- Avoid user-facing text as a locator (e.g.,
cy.contains("Sign In")
) because marketing or UX tweaks might break your test.
Why It Helps
- Developers: Less time fixing broken selectors whenever a designer changes class names.
- QA: Reliable locators mean fewer false negatives.
- Product Managers: Feature updates can roll out faster with tests that don’t crumble under small UI tweaks.
For deeper guidance, see Cypress Best Practices: Selecting Elements or Playwright: Locators.
2. Maintain a Clear, Consistent HTML Structure
Common Pitfall
Endless nested <div>
tags, random IDs, and unclear naming conventions force testers to craft brittle locators—like div:nth-child(3) > div > button
.
What to Do Instead
- Use semantic HTML5 elements (
<header>
,<main>
,<section>
,<footer>
, etc.). - Keep nesting shallow and avoid auto-generated IDs that change on each build.
Why It Helps
- Developers: Easier to navigate and maintain code.
- QA: Predictable DOM structure reduces complexity in writing tests.
- Organization: Cleaner code base fosters quicker onboarding for new team members.
Learn more at MDN: HTML5 Semantic Elements.
3. Separate UI from Business Logic
Real-World Example
A company had all its domain logic (like user authentication) directly in React components. A single UI refactor caused massive ripple effects in their test suite. By extracting that logic into an API layer, they reduced test failures by over 40% on subsequent refactors.
What to Do
- Expose business rules through a stable API (e.g., REST, GraphQL).
- Keep front-end code focused on rendering and minimal state handling.
Why It Helps
- Developers: Can test domain logic independently (unit or integration tests) without worrying about UI changes.
- QA: Gains a consistent backend interface for data setup or validation.
- Product Managers: Features can be iterated on quickly because UI redesigns won’t derail underlying logic.
Reference: Martin Fowler on PresentationDomainSeparation.
4. Provide Utilities for Test Data Setup
Common Pitfall
Spending 10 steps in the UI to create a test user before testing a core scenario. If something breaks mid-setup, your entire flow is blocked.
What to Do
- Create dedicated test endpoints (e.g.,
POST /api/test-setup/create-user
) to seed users, products, or other data in seconds. - Leverage fixture files: Preload databases with known states (like sample user accounts or product listings).
Why It Helps
- Developers: Faster local testing—no need to manually create data repeatedly.
- QA: Can focus on verifying actual scenarios rather than wrestling with data setup.
- Product Managers: Quicker test cycles mean rapid feature validation and release.
See Cypress Docs on test data requests.
5. Adopt a Page Object or Screenplay Pattern
Before vs. After
Before (Hardcoded Selectors Everywhere)
it('logs in the user', () => {
cy.visit('/login');
cy.get('[data-test="username-input"]').type('user1');
cy.get('[data-test="password-input"]').type('pass123');
cy.get('[data-test="login-button"]').click();
// repeated in multiple tests...
});
After (Page Object Abstraction)
class LoginPage {
elements = {
usernameInput: () => cy.get('[data-test="username-input"]'),
passwordInput: () => cy.get('[data-test="password-input"]'),
submitButton: () => cy.get('[data-test="login-button"]'),
};
login(username, password) {
this.elements.usernameInput().type(username);
this.elements.passwordInput().type(password);
this.elements.submitButton().click();
}
}
export default new LoginPage();
// Test
import LoginPage from '../pageObjects/loginPage';
it('logs in the user', () => {
LoginPage.login('user1', 'pass123');
});
Why It Helps
- Developers: A single place to maintain selectors if the UI changes.
- QA: Tests become more readable, like a mini specification.
- Product Managers: Faster turnaround for new features—UI changes don’t cause a chain reaction of broken tests.
Explore Selenium’s Page Object Model or the Screenplay Pattern.
6. Decouple Visual Elements from Automation Locators
Reality Check
Text-based selectors (cy.contains("Sign In")
) are tempting but risky—marketing might change that button text to “Log In” and break half your tests overnight.
What to Do
- Designate stable attributes (
data-test="login-button"
) for every clickable element or field you want to automate. - Keep classes and text for styling and user-facing content, not test logic.
Why It Helps
- Developers & QA: Less test maintenance when UI text or CSS evolves.
- Product Managers: Freed to change copy at will without crippling the test suite.
7. Consider Cross-Browser and Cross-Device Early
Real-World Example
A retail site worked great on Chrome but had major layout issues on Safari iOS. Because they never tested Safari in CI, they found out too late—impacting iPhone users at launch.
What to Do
- Use tools like Selenium Grid, BrowserStack, or Playwright to run tests across multiple browsers and devices.
- Include mobile viewport testing if you’re shipping a responsive or mobile-first site.
Why It Helps
- Developers: Fewer last-minute surprises with Safari or older browsers.
- QA: Automated coverage of different environments.
- Product Managers: Confident that the user experience is consistent across platforms.
8. Version and Deploy Consistently
The Danger of “Version Confusion”
Ever had QA test “the latest build,” only to realize they’re running last week’s code on the staging server?
What to Do
- Adopt a clear branching strategy (Gitflow or trunk-based) and label merges or releases.
- Set up CI/CD pipelines: Automatically test each pull request; once merged, deploy to staging with a version tag.
Why It Helps
- Developers: Everyone knows exactly what code is in staging or production.
- QA: Test results map directly to a commit or version.
- Product Managers: Tracking progress is easier; rollback is simpler if needed.
9. Minimize (or Carefully Handle) Dynamic and Asynchronous Elements
Common Pitfall
Flaky tests that say “element not found” or “element is not clickable” occur because the DOM hasn’t fully loaded or an animation is still running.
What to Do
- Use explicit waits that tie to a real condition (e.g.,
cy.get('[data-test="spinner"]').should('not.exist')
). - Provide consistent loading indicators that appear and disappear once data is ready.
Why It Helps
- Developers: Clear design patterns around loading states.
- QA: Fewer random timeouts and test failures.
- Product Managers: Smooth user experience that’s guaranteed to be tested thoroughly.
10. Document Your Testing Hooks and Strategy
Real-World Example
A new hire on the QA team spends days deciphering how to locate elements or seed data, delaying test coverage of new features. This frustration is entirely avoidable.
What to Do
- Maintain a README that outlines:
- Which attributes to use for tests (e.g.,
data-test="..."
). - How to run local tests.
- How to set up or reset test data.
- Which attributes to use for tests (e.g.,
- Use a wiki or knowledge base for deeper architecture and environment overviews.
Why It Helps
- Developers & QA: Shared understanding prevents confusion and wasted time.
- Organization: Smoother onboarding leads to higher velocity and fewer mistakes.
Consider Confluence or a GitHub Wiki for structured documentation.
Conclusion: Small Changes, Big Impact
By taking these steps—unique data-test
attributes, a clean HTML structure, stable test data APIs, and well-documented best practices—you’re not just making QA’s life easier, you’re empowering your entire organization:
- Developers save time by avoiding repeated test fixes.
- QA gains speed and confidence with stable, maintainable tests.
- Product Managers see fewer delays, smoother releases, and happier customers.
Next Steps
- Pick one of these ideas (like adding
data-test
attributes to your critical elements) and implement it this week. - Measure how it affects test reliability or developer/QA satisfaction.
- Gradually adopt more best practices until your site is fully “test-friendly.”
By designing with testability in mind, you’ll transform those dreaded late-night fire drills into smooth, efficient releases—and keep your team moving forward with confidence.
Author Of article : Donald Johnson Read full article