Skip to content
QSWEQB
easyConceptCodingEntryMid

What belongs in a page object, and what should never go in one?

A page object owns locators and the mechanics of interacting with a screen, and nothing else. Assertions, test data and workflow belong in the test, because the moment a page object asserts, the test's intent stops being readable from the test.

4 min readUpdated 2026-07-26

What the interviewer is scoring

  • Whether the candidate keeps assertions out of the page object and can say why
  • That locators are treated as the thing being encapsulated rather than the page's appearance
  • Does the candidate distinguish a page object from a higher-level task or flow abstraction
  • Whether returning the next page object is discussed as a trade-off rather than a rule
  • That locator strategy is connected to how often the object needs changing

Answer

The one job

A page object exists so that when the markup changes, exactly one file changes. It encapsulates the locators for a screen and the mechanics of driving it — click this, type into that, read this value — and exposes those as methods named for what a user does.

Everything else you might put in it belongs somewhere else, and the reason is consistent: anything that varies per test does not belong in an object shared by every test.

public final class LoginPage {
    private final Page page;
    LoginPage(Page page) { this.page = page; }

    // Vocabulary of the user, not of the DOM. No assertion, no test data.
    public void signIn(String username, String password) {
        page.getByLabel("Username").fill(username);
        page.getByLabel("Password").fill(password);
        page.getByRole(AriaRole.BUTTON, new GetByRoleOptions().setName("Sign in")).click();
    }

    public String errorMessage() {
        return page.getByTestId("login-error").textContent();
    }
}

Note that errorMessage returns the text rather than checking it. The page object's job is to make the state available; deciding what the state should be is the test's job.

Why assertions are the line

Putting an assertion in a page object is the most common mistake and the one with the widest blast radius, so it is worth being precise about the harm.

A test that reads loginPage.signIn(user, pass) and then asserts, in the test body, that the dashboard is shown, tells you what it is verifying by being read. A test that calls loginPage.signInAndVerifyDashboard(...) tells you nothing without opening another file, and the check it performs is now the same for every test that calls it — including the one that wanted to verify something slightly different and could not.

There is a compounding effect. Once a page object asserts, it needs variants: one for the success case, one for the locked-account case, one for the password-expiry case. The object accumulates methods that are really test scenarios, the tests become one-line calls to those scenarios, and the suite's intent has moved entirely out of the tests. At that point the page object is a test suite wearing the wrong name.

The narrow exception is a wait, which reads like an assertion and is not. waitForLoaded() is a synchronisation concern and is mechanics. It fails when the page never loads, which is a different thing from failing because the page loaded with the wrong content.

Test data does not belong either

Hard-coding a username inside a page object couples every test to one account. Someone then adds signInAsAdmin(), and the object now encodes which users exist, which is knowledge that belongs in the test or in a fixture.

Pass the data in. The page object should not know that admins exist; it should know that there is a username field.

Flows are a separate layer

A checkout spanning four screens does not belong in any one page object, and putting it in the first one is arbitrary. The clean structure has two levels: page objects for mechanics, and a task or flow layer above them that composes several pages into something meaningful — completeCheckout(cart, card) calling four page objects in sequence.

Getting this wrong is what makes page objects bloat. Without the flow layer, multi-page journeys get pushed into whichever page object seems closest, and each one grows methods that reach into pages it should not know about.

The related question is whether an action should return the next page object. Chaining reads well and encodes navigation in the type system, which catches a class of mistake at compile time. It also means every page object imports the pages it can navigate to, so the objects become a tangle of mutual references, and a single navigation change ripples. Returning the next page is reasonable where navigation is genuinely deterministic; where a click can land on one of three screens depending on state, forcing a single return type is a lie and the flow layer should decide.

Locators are the real maintenance cost

The whole value proposition is that markup changes are absorbed in one place, and how well that works depends entirely on locator choice.

An XPath describing a path through the DOM breaks when anyone adds a wrapper div, which is the most common change a front-end team makes. A CSS class breaks when styling is refactored, and with utility-class frameworks the class names carry no semantic meaning at all.

Prefer locators tied to what the element is rather than where it sits: an accessible role and name, a label, or a dedicated test attribute. Role and label locators have a second benefit — they only work if the page is marked up accessibly, so a locator that breaks is often telling you about a real accessibility regression. A data-testid is the pragmatic fallback and needs an agreement with the front-end team that those attributes are a contract and not clutter to be tidied away.

When to skip them

Page objects earn their cost through reuse. A screen touched by one test does not need one, and a small suite of a dozen tests may be clearer without the indirection. The pattern pays off at scale and in longevity, which is also why it is worth doing properly rather than half-way — a page object stuffed with assertions and test data is more expensive than none at all, because it has the maintenance cost of an abstraction and none of the clarity.

Locators and mechanics in the page object, intent in the test. The moment a page object starts verifying things, you can no longer tell what a test is testing by reading it.

Likely follow-ups

  • Your login method asserts the dashboard loaded. What breaks about that?
  • Where does a multi-page checkout flow live if not in a page object?
  • How do you choose a locator that survives a front-end rewrite?
  • When would you not use page objects at all?

Related questions

page-objecttest-automationmaintainabilitylocatorsselenium