automationpage-objectarchitectureplaywrightseleniumqa

UI Test Automation Architecture: Page Object and What Comes After It

My first automation suite looked honest and simple: open the page, find the field, type text, click the button, check the result. Twenty tests — beautiful, all green, written in an evening. The trouble starts later, and always the same way: you have two hundred tests, a designer changes the login button, and you’re fixing the same selector in forty files. Or login moves from a separate page to a modal — and half the suite goes red, not because of a bug, but because “log in” is done differently now.

This is usually when the thought about “test automation architecture” arrives. And with it, the important thing is not to overdo it — in either direction.

Why architecture at all

Let’s first agree on why. Not for beauty, and not because “that’s how it’s done.” A good automation suite is measured not by the number of tests but by the cost of change — how many places you have to touch when one thing changes in the product. If the answer is “one” — the architecture is good. If it’s “search the whole project and forty edits” — it’s bad, no matter how many fashionable patterns you hang on top.

Page Object — what it actually is

A Page Object is usually explained as “a class per page.” That’s a description, not the essence. The essence: a Page Object hides HOW behind WHAT. The test says what the user does — “log in as so-and-so,” “add an item to the cart.” How exactly that happens in the DOM — which selectors, clicks, waits — is known only to the Page Object. The test shouldn’t know that the login button is button[data-testid=login]. It only knows loginPage.loginAs(user).

The side effect of this separation is exactly what fixes the pain from the intro. The button’s selector lives in one place. The markup changed — you fix one method in one object, not forty tests.

What goes in a Page Object, and what doesn’t

The most common anti-pattern is assertions inside the Page Object. The temptation is clear: the object knows the page, so let it check too. But then the Page Object starts deciding what is important to verify — and that’s the test’s job. The right way: the Page Object returns state (error text, whether an element is visible, how many rows in the table) or returns another Page Object (after a successful login — DashboardPage). Comparing against an expectation is the test’s job.

The one exception I allow myself is checking that we’re on the right page at all (a basic “the page loaded”): it’s convenient to keep that in the object so you fail early and clearly.

And one more thing: a Page Object is about behavior, not a map of elements. An object with fifty locator fields and not a single action method isn’t a Page Object — it’s a dictionary of selectors. The value is in methods that speak the user’s language.

The layers above Page Object

Page Object isn’t the ceiling. As the suite grows, things appear that don’t fit the “page” model.

Components. Header, footer, confirmation modal, toast, date picker — pieces that live on many pages. Keeping them in every Page Object is copy-paste. Extract them into separate component objects, and let pages reuse them. Then “close the cookie banner” is described once.

Setup via API, not UI. The most underrated part. If a test needs a logged-in user with an item in the cart — don’t click through login and adding an item via the interface in every test. It’s slow and fragile: registration breaks, and everything goes red, even though you were testing something else entirely. Prepare state via API or the database, and use the UI to do exactly what you’re testing. Login itself is enough to check in a couple of dedicated login tests.

Test data. Hardcoding “[email protected] / Password123” spreads and couples tests to each other: two tests fight over one user — hello, flakiness. Better to use factories and builders that create fresh data for a specific test, with uniqueness where it matters (email with a uuid/timestamp). There was a separate post about this — test data quietly breaks a suite harder than locators do.

Where people over-engineer

Now, honestly, about the opposite extreme — I see it more often than “no architecture at all.” A BasePage that all pages inherit from, bloated to thirty “utility” methods used by three pages out of thirty. Inheritance for the sake of inheritance. Wrappers over wrappers over the driver — a “custom framework” on top of Playwright, which is already a framework. Abstractions invented in advance, for duplication that doesn’t exist yet.

The rule that saves me: don’t abstract in advance. First repetition — tolerate it. Second — notice it. On the third — extract it. Before the third time you don’t know which abstraction is right, and you’ll almost certainly guess wrong — and breaking a bad abstraction costs more than removing copy-paste.

And the criterion above it: an abstraction should simplify reading the test, not add a layer you have to dive into. If, to understand a test’s scenario, you have to open three files — the architecture is working against you.

What a healthy test looks like

A simple criterion I use to judge a suite: open a test and read it aloud. If it comes out as a coherent user scenario — “logged in, added an item, placed an order, saw the confirmation” — with not a single selector, css, or xpath in the test body — the architecture is in place. If //div[2]/span and sleep(3000) stick out of the test — it isn’t, no matter how many Page Objects lie next to it.

In short — what to take with you

  • Locators in one place (Page Object or component). The test doesn’t know about css.
  • Page Object without assertions: it returns state or the next object; the test verifies.
  • Setup via API/DB, and via UI only what you’re actually testing.
  • Repeated UI pieces are components, not copy-paste across pages.
  • Don’t abstract in advance — the rule of three: a working duplicate first, then the abstraction.
  • A test reads like a user scenario. A selector in the test body is a smell.

Further reading: Martin Fowler — PageObject · Playwright — Page Object Models · Selenium — Page Object Models