Automation First App Design Framework & Best Practices

A concept promoting how developers can design their apps to be automation-friendly from day zero – a tester’s perspective.

Introduction

As an automation tester, I’ve spent countless hours wrestling with applications that seemed deliberately designed to resist automation. Brittle selectors that break with minor UI tweaks, components without identifiable properties, and complex workflows hidden behind unclear states—these are the daily frustrations we face.

But here’s the thing: most of these challenges are preventable. When developers design applications with automation in mind from the start, testing becomes faster, more reliable, and significantly more maintainable.

This isn’t just about making our jobs easier—it’s about delivering higher-quality software faster.

This article shares practical recommendations that every application developer should follow to make their applications “automation ready.”

Make Elements Identifiable and Stable

The Problem:

Many apps rely solely on CSS class names or generated IDs that change with every build. Automation frameworks like Selenium, Playwright, and Appium depend on locators (unique identifiers) to interact with elements.

What Developers Should Do:

  • Add data-testid attributes to interactive elements (buttons, inputs, forms, links)
  • Keep these IDs stable across releases—treat them like public APIs
  • Avoid relying only on dynamic class names or XPath positions

Real Example:

// ❌ BAD - Changes frequently 

<button className="btn-primary-v2 btn-submit-form"> 

Submit 

</button> 

// ✅ GOOD - Stable identifier for testers 

<button data-testid="form-submit-button" className="btn-primary-v2"> 

Submit 

</button>

Submit

Why This Matters:

When you add data-testid, testers can reliably locate elements using page.locator('[data-testid="form-submit-button"]') in Playwright or find_element(By.CSS_SELECTOR, '[data-testid="form-submit-button"]') in Selenium.

Use Semantic HTML and Proper Label Associations

The Problem:

Generic <div> and <span> and elements without labels make it nearly impossible for testers to understand the purpose of controls and for accessibility tools to function correctly.

What Developers Should Do:

  • Use semantic HTML elements: <button>, <input>, <label>, <form>
  • Associate labels with inputs using <label for="inputId">
  • Use aria-label or aria-labelledby for complex components

Real Example:

// ❌ BAD - No semantic meaning 

<div onClick={() => setSelected(!selected)}> 

<span>Enable notifications</span> 

</div> 

 

// ✅ GOOD - Semantic and testable 

<label htmlFor="notifications-toggle"> 

<input  

id="notifications-toggle"  

type="checkbox"  

data-testid="notifications-toggle" 

/> 

Enable notifications 

</label>

Tester’s Perspective:

With semantic HTML, I can use role-based selectors: page.locator(‘input[role=”checkbox”]’) or even getByRole(“checkbox”, { name: /enable notifications/i }) in Playwright—making tests more readable and resilient to style changes.

Implement Explicit Wait States and Loading Indicators

The Problem:

JavaScript-heavy applications load content dynamically. Testers often encounter flaky tests because elements appear/disappear asynchronously, and they don’t know when the page is truly “ready.”

What Developers Should Do:

  • Use data-testid for loading states:
    <div data-testid="loading-spinner"> 
  • Mark content sections with status attributes: aria-busy="true" during load
  • Ensure the app reaches a stable state before responding to user interactions
  • Provide explicit loading completion signals

Real Example:

// ❌ BAD - No semantic meaning 

<div onClick={() => setSelected(!selected)}> 

<span>Enable notifications</span> 

</div> 

 

// ✅ GOOD - Semantic and testable 

<label htmlFor="notifications-toggle"> 

<input  

id="notifications-toggle"  

type="checkbox"  

data-testid="notifications-toggle" 

/> 

Enable notifications 

</label>

Tester’s Automation Code (Playwright):

// Wait for loading to complete 

await page.locator('[data-testid="loading-spinner"]').waitFor({ state: 'hidden' }); 

 

// Now interact with the list 

const userList = page.locator('[data-testid="user-list"]'); 

await expect(userList).toBeVisible();

Design Forms with Clear Input Handling 

The Problem: 

Complex forms with unclear error states, hidden validations, or non-standard input behaviors confuse automation scripts. 

What Developers Should Do: 

  • Use standard HTML form elements with clear name attributes 
  • Provide explicit error messages with data-testid markers 
  • Ensure form validation is observable (not silent failures) 
  • Include success confirmations 

Real Example: 

// ✅ GOOD - Clear form with observable states 

<form data-testid="login-form"> 

<div> 

<label htmlFor="email-input">Email:</label> 

<input 

id="email-input" 

name="email" 

type="email" 

data-testid="email-input" 

required 

/> 

{errors.email && ( 

<span data-testid="email-error" role="alert"> 

{errors.email} 

</span> 

)} 

</div> 

 

<div> 

<label htmlFor="password-input">Password:</label> 

<input 

id="password-input" 

name="password" 

type="password" 

data-testid="password-input" 

required 

/> 

</div> 

 

<button  

type="submit"  

data-testid="login-submit-button" 

disabled={isSubmitting} 

> 

{isSubmitting ? 'Logging in...' : 'Login'} 

</button> 

 

{submitSuccess && ( 

<div data-testid="login-success-message" role="status"> 

Login successful! Redirecting... 

</div> 

)} 

</form>

Tester’s Automation Code (Selenium):

from selenium import webdriver 

from selenium.webdriver.common.by import By 

from selenium.webdriver.support.ui import WebDriverWait 

from selenium.webdriver.support import expected_conditions as EC 

 

driver = webdriver.Chrome() 

driver.get("https://app.com/login") 

 

# Fill form 

driver.find_element(By.CSS_SELECTOR, '[data-testid="email-input"]').send_keys("test@example.com") 

driver.find_element(By.CSS_SELECTOR, '[data-testid="password-input"]').send_keys("password123") 

 

# Submit 

driver.find_element(By.CSS_SELECTOR, '[data-testid="login-submit-button"]').click() 

 

# Wait for success message 

WebDriverWait(driver, 10).until( 

EC.presence_of_element_located((By.CSS_SELECTOR, '[data-testid="login-success-message"]')) 

)

Make API Responses Predictable and Consistent

The Problem:

Mobile and web apps that depend on API responses need stable data structures. Inconsistent or unpredictable API responses cause tests to fail randomly.

What Developers Should Do:

  • Use consistent JSON structure across all endpoints
  • Include unique identifiers (IDs) in every object
  • Document API contracts (OpenAPI/Swagger)
  • Return meaningful HTTP status codes and error messages
  • Test API responses during development

Real Example:

// ✅ GOOD - Predictable API response 

{ 

"success": true, 

"data": { 

"id": "user-123", 

"email": "test@example.com", 

"name": "John Doe", 

"created_at": "2026-03-01T10:00:00Z" 

}, 

"timestamp": "2026-03-22T14:30:00Z" 

} 

 

// ❌ BAD - Inconsistent response 

{ 

"status": "ok", 

"user": { 

"uid": "123", 

"email_address": "test@example.com" 

}, 

"meta": { "time": 1679574600 } 

}

Tester’s Mobile Automation (Appium):

# With predictable API, testers can mock and validate 

from appium import webdriver 

 

driver = webdriver.Remote("http://localhost:4723/wd/hub", capabilities) 

 

# Mock the API response 

mock_response = { 

"success": True, 

"data": { 

"id": "user-123", 

"name": "Test User" 

} 

} 

 

# Now reliably access user data in the app 

user_name_element = driver.find_element("xpath", "//text[@value='Test User']")

Provide Test-Friendly Configuration & Modes

The Problem:

Production features like analytics, ads, third-party trackers, and network delays can interfere with test execution.

What Developers Should Do:

  • Provide a test mode or environment variable
  • Allow disabling analytics, ads, and tracking in test environments
  • Support skipping unnecessary animations or delays
  • Provide API endpoints to reset app state for testing

Real Example (React):

// ✅ GOOD - Test-friendly configuration 

const isTestMode = process.env.REACT_APP_TEST_MODE === 'true'; 

 

function App() { 

return ( 

<div> 

{!isTestMode && <Analytics />} 

{!isTestMode && <Ads />} 

<MainContent /> 

</div> 

); 

}

Environment Variable:

REACT_APP_TEST_MODE=true npm start

Document Automation Expectations

The Problem:

Testers spend time reverse-engineering how to interact with an application, especially custom components.

What Developers Should Do:

  • Document data-testid naming conventions in your README
  • Provide a test/automation guide for complex features
  • Share component documentation (Storybook, etc.)
  • List all custom events and callbacks that testers should know about

Example Documentation:

## Automation Testing Guide 

 

### Naming Conventions 

- Buttons: `data-testid="[feature]-[action]-button"` (e.g., `user-logout-button`) 

- Form inputs: `data-testid="[form]-[field]-input"` (e.g., `login-email-input`) 

- Lists: `data-testid="[list]-container"` and items as `[list]-item-[id]` 

 

### Key Components 

- **Modal**: `.modal-overlay` (hidden when closed), contains `data-testid="modal-close-button"` 

- **Dropdown**: Use semantic `<select>` or `aria-haspopup="listbox"` 

- **Date Picker**: Supports keyboard navigation and `aria-label` 

 

### Test Environment Setup 

Set `API_URL=http://localhost:3001/api` to use mock server

Key Takeaways for Developers

What Testers Need What Developers Should Do
Stable element identifiers Use data-testid attributes
Clear component hierarchy Use semantic HTML
Observable loading/ready states Provide loading indicators with identifiers
Predictable form behavior Document validation and use standard elements
Consistent API responses Follow API contracts, document structure
Test-friendly environment Support test mode, disable ads/analytics
Clear automation guidance Document your automation expectations

Conclusion

Building automation-ready applications isn’t about working around limitations—it’s about designing applications correctly from day zero.

When developers prioritize clarity, consistency, and observability, everyone wins – testers write faster and more reliable tests; users get higher-quality software, and the entire team moves quicker.

The good news? Most of these practices are good software engineering practices anyway. Semantic HTML, predictable APIs, and clear error handling benefit accessibility, performance, and user experience—not just automation testing.

Start with data-testid attributes, use semantic HTML, make loading states visible, and document your components. Your automation testers will thank you, and your application will be better for it.

Resources & Tools

You Might Also Like