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']}")
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.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.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.