POM Best Practices and Anti-Patterns — Rules That Keep Your Framework Clean

POM is simple in concept but requires discipline in practice. Over time, teams introduce shortcuts that erode the pattern’s value — exposing the driver directly, stuffing assertions into page objects, creating mega-pages with hundreds of locators, or skipping return types. These anti-patterns gradually transform a clean framework into an unmaintainable mess. This lesson catalogues the golden rules and the anti-patterns, giving you a checklist to audit your own POM implementation.

POM Golden Rules and Anti-Patterns

Following these rules keeps your POM framework clean as it grows from 5 page objects to 50. Violating them creates the same maintenance problems POM was designed to prevent.

# POM Golden Rules and Anti-Patterns

GOLDEN_RULES = [
    {
        "rule": "1. One page object per page or major component",
        "why": "Keeps classes focused and manageable (5-20 locators, 5-15 methods)",
        "example": "LoginPage, InventoryPage, CartPage — not ApplicationPage",
    },
    {
        "rule": "2. Locators are private class attributes, never exposed to tests",
        "why": "Tests should call action methods, not know about By.ID or CSS selectors",
        "example": "Test calls login_page.login(u, p) — never login_page.USERNAME_INPUT",
    },
    {
        "rule": "3. Methods return page objects, not raw elements or None",
        "why": "Models page transitions; enables fluent chaining; catches nav failures",
        "example": "login() returns InventoryPage; add_to_cart() returns self",
    },
    {
        "rule": "4. No assertions in page objects",
        "why": "Page objects are reusable; different tests have different expectations",
        "example": "get_product_count() returns int; test asserts == 6",
    },
    {
        "rule": "5. No raw Selenium calls in tests",
        "why": "Tests should read like business requirements, not API documentation",
        "example": "Test uses page.login(), not driver.find_element().send_keys()",
    },
    {
        "rule": "6. Inherit from BasePage for shared behaviour",
        "why": "Eliminates duplicated wait/click/type logic across all page objects",
        "example": "LoginPage(BasePage) inherits click(), type_text(), get_text()",
    },
    {
        "rule": "7. Use descriptive method names that reflect user actions",
        "why": "Tests become self-documenting and reviewable by non-technical team members",
        "example": "add_product_to_cart() not click_button_3()",
    },
    {
        "rule": "8. Verify page load in the constructor",
        "why": "Catches navigation failures immediately with a clear error message",
        "example": "InventoryPage.__init__ waits for inventory_list visibility",
    },
]

ANTI_PATTERNS = [
    {
        "anti_pattern": "The God Page Object",
        "symptom": "One page object with 200+ locators and 100+ methods for the entire app",
        "fix": "Split into one class per page; use composition for shared components (nav bar)",
    },
    {
        "anti_pattern": "Driver Leakage",
        "symptom": "Tests access page_object.driver directly to call find_element()",
        "fix": "Make driver private; add missing action methods to the page object instead",
    },
    {
        "anti_pattern": "Assertion Contamination",
        "symptom": "Page object methods contain assert statements",
        "fix": "Return data from page objects; move all assertions to test methods",
    },
    {
        "anti_pattern": "Hardcoded Waits in Page Objects",
        "symptom": "time.sleep(5) scattered throughout page object methods",
        "fix": "Use WebDriverWait with expected conditions in BasePage methods",
    },
    {
        "anti_pattern": "Locator Strings in Tests",
        "symptom": "Tests pass CSS selectors or XPaths as string arguments to page objects",
        "fix": "Locators are defined inside the page object class, never passed from tests",
    },
    {
        "anti_pattern": "Missing Return Types",
        "symptom": "Navigation methods return None; test must manually create next page object",
        "fix": "Navigation methods return the destination page object instance",
    },
]

print("POM Golden Rules")
print("=" * 65)
for r in GOLDEN_RULES:
    print(f"\n  {r['rule']}")
    print(f"    Why: {r['why']}")
    print(f"    Example: {r['example']}")

print("\n\nPOM Anti-Patterns to Avoid")
print("=" * 65)
for ap in ANTI_PATTERNS:
    print(f"\n  Anti-pattern: {ap['anti_pattern']}")
    print(f"    Symptom: {ap['symptom']}")
    print(f"    Fix: {ap['fix']}")
Note: The “Driver Leakage” anti-pattern is the most common way teams gradually abandon POM. It starts innocently: a test needs to do something the page object does not support, so the developer writes page.driver.find_element(...) as a quick fix. Over time, more tests bypass POM for “just this one case,” and eventually half the test suite uses raw Selenium while the other half uses page objects. The fix is cultural: when a test needs an interaction that the page object does not provide, add the method to the page object — do not bypass it.
Tip: For shared UI components that appear on multiple pages (navigation bar, footer, notification toasts), create separate component classes rather than duplicating locators across page objects. A NavBar class with methods like go_to_cart() and logout() can be composed into any page object via self.nav = NavBar(driver). This composition pattern keeps page objects lean and avoids the “every page object has the same 20 nav bar locators” duplication.
Warning: Do not over-engineer your POM framework before you have enough tests to justify it. Start with BasePage and two or three page objects. Add patterns (composition, factories, page component inheritance) only when real complexity demands them. A framework with three design patterns and five abstraction layers for a 20-test suite is over-engineered — it takes longer to understand than the tests it supports. Let the framework grow organically with the test suite.

Common Mistakes

Mistake 1 — Over-engineering the framework before writing tests

❌ Wrong: Spending two weeks building a framework with page factories, abstract base classes, custom decorators, and configuration managers before writing a single test.

✅ Correct: Starting with BasePage + LoginPage + two tests. Adding patterns (composition, parameterisation, reporting) as the test count grows and real needs emerge. Framework complexity should follow test complexity, not precede it.

Mistake 2 — Not reviewing page objects during code reviews

❌ Wrong: Only reviewing test files during pull requests and ignoring changes to page objects.

✅ Correct: Reviewing page objects with the same rigour as test files. A bad locator, a missing wait, or an assertion hidden in a page object can silently break dozens of tests. Page object reviews should check for all eight golden rules.

🧠 Test Yourself

A test calls login_page.driver.find_element(By.ID, "price").text to read a price because the InventoryPage does not have a get_price() method. What POM anti-pattern is this?