DEV Community

Cover image for Should Page Objects Assert? Where Test Assertions Belong
Anton Gulin
Anton Gulin

Posted on • Originally published at anton.qa

Should Page Objects Assert? Where Test Assertions Belong

Should a page object contain assertions? Here is the short answer: business assertions belong in the test. Technical guards belong in the page object. A test must show what "correct" means for its scenario. A page object must only promise that the page is ready to use. Mixing the two is why suites become hard to read and hard to trust.

Last week I published the modern page object model. One sign of legacy design was "page objects that assert." A senior QA engineer pushed back in the comments. His position: business checks go in the test layer, but technical checks, did the page reach the right state?, can hide inside the class.

It was a fair challenge. This post is the full answer.

The two kinds of checks

Every check in a UI test is one of two kinds.

Business checks answer: did the product do the right thing? The order total is $41.97. The welcome message names the user. The discount applied.

Technical guards answer: is the page ready? The form finished loading. The spinner went away. The URL changed.

They look similar in code. They serve different readers. A business check speaks to the person deciding "is this feature broken?" A technical guard speaks to the machine deciding "can I click now?"

The rule

Put business checks in the test, always:

test('applies the discount', async ({ checkoutPage }) => {
  await checkoutPage.applyCode('SAVE10');
  await expect(checkoutPage.total).toHaveText('$35.97');
});
Enter fullscreen mode Exit fullscreen mode

The expected value sits in the test file. When this fails at 2 a.m., the reader sees what "correct" was supposed to be. No file jumping.

Handle technical guards inside the page object, but prefer waiting over asserting:

export class CheckoutPage {
  readonly total: Locator;

  async applyCode(code: string) {
    await this.codeInput.fill(code);
    await this.applyButton.click();
    await this.priceUpdate.waitFor({ state: 'visible' });
  }
}
Enter fullscreen mode Exit fullscreen mode

The page object does not judge the total. It makes one promise: when applyCode returns, the page finished reacting. That is a technical guard, and note it is a wait, not an assert. Playwright's web-first assertions and auto-waiting handle most of these guards for free.

Why hidden business asserts hurt

Three costs show up at scale.

The expected value disappears. checkoutPage.verifyTotal() hides $35.97 in another file. The failing test cannot tell you what it believed.

The page object takes sides. Fifty tests share that class. One scenario needs a different expected total, and the shared method becomes a maze of parameters.

Failures point at the wrong layer. When an assert fires inside a page object, the stack trace blames plumbing. The reader has to dig to find which business rule broke.

Where it gets honestly debatable

My commenter's position (hidden technical assertions are fine) is workable. Plenty of strong suites do it. My preference is stricter for one reason: an assert stops the test with a verdict; a wait just holds the door. Verdicts belong to tests. But if your team hides technical guards as asserts and everyone can read the failures, that is a style choice, not a defect.

What is a defect: expected business values living anywhere except the test body.

The migration path

Same as the POM modernization: no big rewrite. When a change touches a page object that asserts, move the business expectation up into the tests that call it, and convert the technical remainder into a wait. Each class takes minutes.

Run this to find your candidates:

grep -rn "expect(" src/pages/
Enter fullscreen mode Exit fullscreen mode

Every hit is either a business check to promote or a guard to convert.

The takeaway

  • Business checks: in the test, expected values written out.
  • Technical guards: in the page object, as waits, not verdicts.
  • Playwright's auto-waiting already covers most guards, delete before you migrate.

Anton Gulin is the AI QA Architect, the first person to claim this title on LinkedIn. He builds AI-powered test automation systems where AI agents and human engineers collaborate on quality. Former Apple SDET (Apple.com / Apple Card pre-release testing). Find him at anton.qa or on LinkedIn.

Top comments (0)