muyoy · blog

Writing Maintainable Automated Tests with Playwright

Writing Maintainable Automated Tests

Automated testing is key to accelerating deployment cycles while preserving system quality. However, UI tests can become fragile and prone to false negatives if they are tightly coupled to HTML tags or rely on fixed wait statements.

Using Playwright and structured design patterns, QA engineers can build robust testing suites that are easy to maintain as product layouts evolve.

1. Page Object Model (POM) Pattern

The Page Object Model (POM) is an industry-standard design pattern where page UI elements and actions are encapsulated into class definitions. Test scripts interact with classes instead of executing raw CSS or XPath queries directly.

graph TD
  TestScript["Test: verify_checkout()"] --> CheckoutPage["CheckoutPage Class"]
  CheckoutPage --> Selectors["Selectors & Interaction Methods"]
  Selectors --> ActualWebPage["Actual Browser DOM"]

Below is an example of a login page class using Playwright in Python:

class LoginPage:
def __init__(self, page):
self.page = page
self._username_input = page.locator('#username')
self._password_input = page.locator('#password')
self._login_button = page.locator('button[type="submit"]')
async def navigate(self):
await self.page.goto('/login')
async def login(self, username, password):
await self._username_input.fill(username)
await self._password_input.fill(password)
await self._login_button.click()

If the login button selector changes in the future, you only need to modify it in LoginPage, and all test scripts utilizing the login action will remain functional.

2. Managing Dynamic Content Without Hard Sleeps

Using hard sleep commands (such as time.sleep(5)) leads to flaky and slow test suites:

  • If the network load is slow, the test might need 6 seconds and fail.
  • If the network load is fast, the test wastes time waiting for no reason.

Instead, utilize Playwright’s auto-waiting assertions:

# AVOID:
# await page.wait_for_timeout(5000)
# assert await page.is_visible('.success-message')
# PREFER:
from playwright.sync_api import expect
# Playwright automatically waits up to 5 seconds for this element to appear
expect(page.locator('.success-message')).to_be_visible()

3. Database State Cleanups

A test should always execute in isolation. If a test registers a new user, that user must be deleted from the database at the end of the test.

Leverage PyTest fixtures with yield statements to handle setup and teardown tasks:

import pytest
@pytest.fixture
def test_user_session():
# Setup: Create test user record in database
user_id = db.create_user(email="test@example.com")
yield user_id
# Teardown: Clean up user record
db.delete_user(user_id)

By decoupling test layout logic via POM, avoiding hardcoded delays, and managing test data isolation, you can scale test automation suites that run reliably across multiple platforms.