---
title: POM Anti-Patterns and When to Skip POM | TestingBot
description: The Page Object Model is simple enough that most suites adopt it, and
  flexible enough that most suites eventually misuse it. This chapter collects the…
source_url:
  html: https://testingbot.com/resources/courses/page-object-model/anti-patterns
  md: https://testingbot.com/resources/courses/page-object-model/anti-patterns.md
---

Course

[Page Object Model and Framework Design](https://testingbot.com/resources/courses/page-object-model)

Chapter 9 of 10

1. [1 What Is the Page Object Model?](https://testingbot.com/resources/courses/page-object-model)
2. [2 Your First Page Object in Selenium](https://testingbot.com/resources/courses/page-object-model/first-page-object)
3. [3 Page Object Model in Playwright](https://testingbot.com/resources/courses/page-object-model/playwright-page-objects)
4. [4 Components and Fluent Page Objects](https://testingbot.com/resources/courses/page-object-model/components-and-fluent-pages)
5. [5 Page Factory vs Plain By Locators](https://testingbot.com/resources/courses/page-object-model/page-factory-vs-by-locators)
6. [6 Test Data, Config and Environments](https://testingbot.com/resources/courses/page-object-model/test-data-and-config)
7. [7 Test Automation Framework Structure](https://testingbot.com/resources/courses/page-object-model/framework-layout)
8. [8 Run the Framework in Parallel on a Grid](https://testingbot.com/resources/courses/page-object-model/parallel-on-a-grid)
9. [9 POM Anti-Patterns and When to Skip POM](https://testingbot.com/resources/courses/page-object-model/anti-patterns)
10. [10 Knowledge check](https://testingbot.com/resources/courses/page-object-model/knowledge-check)

[Exit course](https://testingbot.com/resources/courses)

 Chapter 9 of 10 Intermediate 9 min read 3 quick checks Updated Nov 2026 

# POM Anti-Patterns and When to Skip POM
On this page
1. [The assertions debate](https://testingbot.com#the-assertions-debate)
2. [God objects](https://testingbot.com#god-objects)
3. [Over-abstraction](https://testingbot.com#over-abstraction)
4. [Smaller smells](https://testingbot.com#smaller-smells)
5. [When not to use POM](https://testingbot.com#when-not-to-use-pom)

The Page Object Model is simple enough that most suites adopt it, and flexible enough that most suites eventually misuse it. This chapter collects the mistakes that turn page objects from an asset into the thing everyone is afraid to touch, and ends with the cases where a different pattern fits better.

## The assertions debate[#](https://testingbot.com#the-assertions-debate)

Should a page object contain assertions? The three sources most people cite do not agree:

- The **Selenium documentation** : "Page objects themselves should never make verifications or assertions." The one exception is checking that the page "and possibly critical elements on the page, were loaded correctly", done while instantiating the page object.
- **Martin Fowler** (2013): "I favor having no assertions in page objects", with an exception for invariants of the page rather than the things a specific test is checking. He suggests shared assertion helpers to avoid duplication instead.
- **Playwright's POM guide** : its example page object has a `getStarted()` method that clicks a link and then calls `await expect(this.gettingStartedHeader).toBeVisible()`.

These are less contradictory than they look once you separate two kinds of check:

| Kind | Example | Where it belongs |
| --- | --- | --- |
| **Precondition** : "am I where I think I am?" | `LoggedInPage` waits for `/logged-in-successfully/` in its constructor | Page object |
| **Outcome** : "did the feature behave correctly?" | The heading says "Logged In Successfully" | Test |

The Playwright example's `expect` is a precondition: `getStarted()` confirms that navigation finished before the next method acts. That is exactly what our constructor checks do in Selenium, and what Playwright's own auto-waiting would do on the next action anyway.

Outcome assertions inside page objects cause three concrete problems:

1. **The test stops saying what it checks.** `loginPage.loginAndVerifySuccess()` hides the expected result in another file. A reviewer reading the test cannot tell whether "success" means the URL, the heading, or a cookie.
2. **Negative tests get awkward.** A `login()` that asserts success cannot be reused for the wrong-password test, so a second method appears, then a flag parameter, then both.
3. **Failures point at the wrong place.** The stack trace's top frame is in `LoginPage.java`, and the report attributes the failure to the page object rather than to the behaviour.

Our rule for this course, and a reasonable default for yours: page objects may **wait for and verify their own state** ; tests own every **outcome** assertion. If many tests repeat the same group of assertions, extract an assertion helper (`assertLoggedInAs(home, "student")`) in a separate module, not a page object method.

Fifteen tests end with the same three checks that the user is logged in as student. Where should those checks live?

 In an assertion helper module, such as `assertLoggedInAs(home, "student")` In a `LoggedInPage.verifyStudent()` page object method Copied into each test, because duplication in tests is fine

Outcome assertions belong to the test side. A shared helper removes the duplication without turning the page object into a place where failures get attributed to the page.

## God objects[#](https://testingbot.com#god-objects)

The `BasePage` that has grown to 60 methods: `scrollToElement`, `switchToFrame`, `acceptAlert`, `uploadFile`, `waitForSpinner`, `getTableCell`, `clickByJavaScript`, `dragAndDrop`... Every page inherits all of them, nobody knows which are used, and changing one risks every test.

The same happens to page objects for big screens. A 2,000-line `DashboardPage` with 150 locators is not a page object anymore; it is a namespace.

Fixes:

- Keep `BasePage` to waits and the two or three interactions every page needs. Our base page has three helpers.
- Move specialised interactions into **components** (chapter 03): a `FileUploader`, a `DataTable`, a `DatePicker`, each with a root element. Pages that contain one, compose one.
- Prefer composition to inheritance. `CheckoutPage extends CartPage extends ProductPage extends BasePage` makes every page carry its ancestors' locators, and a change to `ProductPage` breaks checkout tests.

`CheckoutPage extends CartPage extends ProductPage`, and a change to `ProductPage` just broke the checkout tests. What is the better structure?

 Add a fourth level so each page has its own parent Compose pages from components, such as a cart summary component, instead of inheriting whole pages Move all locators into `BasePage`

Inheritance makes every page carry its ancestors' locators. Composition shares only the parts that really repeat. Moving everything into `BasePage` creates a god object.

## Over-abstraction[#](https://testingbot.com#over-abstraction)

The opposite failure. Some frameworks wrap every element in its own class (`TextBox`, `Button`, `Link`) or build a generic layer that drives the UI by strings:

```java
// avoid: a framework inside the framework
ui.perform("LoginPage", "username", "type", "student");
ui.perform("LoginPage", "submit", "click", null);
```

This throws away the compiler's help, makes stack traces useless, and forces every new interaction through a layer only its author understands. If a team wants tests written by non-programmers, that is a reason to look at a BDD tool with a proper language layer, not to reimplement one inside the page objects.

Signs of over-abstraction to watch for:

- A page object method that only forwards to another method with the same name.
- Interfaces with a single implementation "in case we add another browser" (WebDriver is already that interface).
- Configuration-driven locators (locators loaded from YAML or a spreadsheet) for a UI that only one team works on. You lose "go to definition" and gain nothing.

Fowler's article names the right level directly: a page object should "allow a software client to do anything and see anything that a human can", while it "hides the underlying widgetry". Methods named after what a user does on the page, in the application's vocabulary, hit that level; a generic string-driven layer misses it.

## Smaller smells[#](https://testingbot.com#smaller-smells)

**Tests that reach past the page object.** A test that calls `driver.findElement(...)` next to page object calls has two sources of truth for that page. When it happens, add the missing method to the page object.

**Page objects that return `WebElement`s** (in Selenium). Tests then click, type and wait on raw elements, and you are back to locator knowledge spread across tests. Return data (strings, numbers, booleans, lists) or other page objects. In Playwright, exposing `Locator`s for web-first assertions is fine, as chapter 02 explained.

**Sleeps.** `Thread.sleep(2000)` or `time.sleep(2)` inside a page object slows every test that uses it and still fails on a slow day. Replace each with a wait for the specific condition the sleep was hoping for.

**Branching on page state.** `if (cookieBanner.isDisplayed()) cookieBanner.accept();` inside a page object makes the test pass whether or not the banner appeared. Decide what the state should be (for instance, set the consent cookie before the test starts) and let the test fail if reality disagrees.

**Page objects holding test state.** A page object that remembers "the user we logged in as" or "the order we created" couples it to one test's story. Keep that in the test or in a fixture.

**Order-dependent tests.** Test B assumes test A already logged in or created data. They pass in sequence and fail in parallel or when run alone. Every test sets up its own preconditions, ideally through fixtures (chapter 05) or an API.

A page object contains `if (cookieBanner.isDisplayed()) cookieBanner.accept();`. What is the problem?

 It is slow because `isDisplayed()` waits It should use `Thread.sleep` first so the banner has time to appear The test passes whether or not the banner appeared; decide the state up front (for example set the consent cookie) and let the test fail if reality disagrees Banners must be handled in `BasePage`

Branching on page state hides real changes in behaviour. Setting the state before the test makes the test deterministic.

## When not to use POM[#](https://testingbot.com#when-not-to-use-pom)

Page objects earn their keep when many tests touch the same screens over a long time. They are overhead when:

- **The suite is tiny or short-lived.** Three smoke tests, or a one-off migration check, do not need a class per page. Write the script; extract page objects when the second test needs the same steps.
- **The test is not about the UI.** API tests and contract tests should call the API directly. Setting up data through the UI to test something else is slow and fragile, even with excellent page objects.
- **The assertion is the whole page.** Visual regression tests compare screenshots; they need a URL and a stable state, not a page object API.

And there is a pattern that some teams prefer for large, behaviour-focused suites: **Screenplay**. Instead of modelling pages, Screenplay models **actors** (a user with abilities, such as "browse the web" or "call the API"), **tasks** they perform ("log in", "add an item to the cart"), and **questions** they ask about the system ("the visible error message"). Tasks are composed from smaller interactions, so the unit of reuse is a business action rather than a screen. Serenity BDD (Java) and Serenity/JS implement it.

Screenplay shines when one business task spans several pages, or several channels (UI plus API), and when tests are written from user stories. The cost is more types and more indirection for simple flows. Many teams find that well-built page objects with components and fluent returns, which is where this course ended up, already get most of the benefit.

If you test native apps, the same anti-patterns apply to screen objects; our [Appium page object article](https://testingbot.com/resources/articles/page-object-model-appium) shows the mobile version of these structures. For a broader look at flaky-test root causes, including several listed above, see [handling test flakiness](https://testingbot.com/resources/articles/handling-test-flakiness).

[Previous · Chapter 8 Run the Framework in Parallel on a Grid](https://testingbot.com/resources/courses/page-object-model/parallel-on-a-grid) [Next · Chapter 10 Knowledge check](https://testingbot.com/resources/courses/page-object-model/knowledge-check)

On this page

1. [The assertions debate](https://testingbot.com#the-assertions-debate)
2. [God objects](https://testingbot.com#god-objects)
3. [Over-abstraction](https://testingbot.com#over-abstraction)
4. [Smaller smells](https://testingbot.com#smaller-smells)
5. [When not to use POM](https://testingbot.com#when-not-to-use-pom)

Certification

This course has a free, hands-on certificate.

[How it works](https://testingbot.com/resources/courses/page-object-model/certification)
