Should Page Objects Assert? Where Test Assertions Belong

Published: · 4 min read

Should page objects contain assertions? A practical rule: business checks live in tests, technical guards live in page objects. With Playwright code.

Dark code-style card asking 'Where do assertions belong?' with a page-object assert marked red and a test-layer expect marked green.

Post

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');
});

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' });
  }
}

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/

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.

playwright · page-object-model · assertions · test-automation · typescript

Subscribe

Get notified when I publish something new, and unsubscribe at any time.

Related articles

Read all my blog posts
Read more about Your AI Model Is a Dependency: Pin It, Keep a Fallback, Re-Verify

·5 min read

Your AI Model Is a Dependency: Pin It, Keep a Fallback, Re-Verify

An AI model can change or vanish under your test suite overnight. The three-rule discipline — pin the version, keep a validated fallback, re-run the same suite on both — explained with the 19-day Fable 5 outage as the case study.

Your AI Model Is a Dependency: Pin It, Keep a Fallback, Re-Verify