Page Object Model (POM) Design
The Page Object Model (POM) is the most widely adopted design pattern in test automation. It separates the concerns of WHAT to test (test logic) from HOW to interact with the UI (page interactions). Without POM, every UI change requires updating dozens of test files. With POM, you update one page class and all tests using it are fixed. This pattern is a mandatory requirement in every professional SDET role.
Why POM Changes Everything
- Problem without POM: 50 test functions all find the login button with 'input[type=submit]'. The dev renames it to 'button[data-testid=login]'. You must update 50 tests.
- Solution with POM: One LoginPage class holds the locator. 50 test functions call loginPage.clickSubmit(). You update ONE line in the class.
- Readability: Tests read like specifications — loginPage.enterEmail('alice@test.com'); loginPage.submit(); dashboardPage.assertWelcomeMessage('Alice');
- Reusability: Any test that needs to log in reuses the same LoginPage.login() method — no duplication
- Maintenance: Locators, actions, and page state live in page classes — never scattered across test files
POM Implementation (Python + Selenium)
# ══════════════════════════════════════════════════════════════
# PROJECT STRUCTURE WITH POM
# ══════════════════════════════════════════════════════════════
# automation/
# pages/
# base_page.py ← Shared methods (wait, screenshot)
# login_page.py ← Login page locators + actions
# dashboard_page.py ← Dashboard page locators + actions
# checkout_page.py ← Checkout page locators + actions
# tests/
# test_login.py ← Login test cases (uses pages/)
# test_checkout.py ← Checkout test cases (uses pages/)
# conftest.py ← driver fixture, base_url
# ── base_page.py ──────────────────────────────────────────────
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
class BasePage:
def __init__(self, driver):
self.driver = driver
self.wait = WebDriverWait(driver, timeout=15)
def click(self, locator):
"""Wait for element to be clickable, then click"""
element = self.wait.until(EC.element_to_be_clickable(locator))
element.click()
def type(self, locator, text):
"""Clear field and type text"""
element = self.wait.until(EC.visibility_of_element_located(locator))
element.clear()
element.send_keys(text)
def get_text(self, locator):
element = self.wait.until(EC.visibility_of_element_located(locator))
return element.text
def is_visible(self, locator):
try:
return self.wait.until(EC.visibility_of_element_located(locator)).is_displayed()
except:
return False
def navigate_to(self, path):
self.driver.get(f"https://staging.myapp.com{path}")
# ── login_page.py ─────────────────────────────────────────────
from selenium.webdriver.common.by import By
from pages.base_page import BasePage
class LoginPage(BasePage):
# All locators defined as class-level constants
EMAIL_INPUT = (By.CSS_SELECTOR, '[data-testid="email-input"]')
PASSWORD_INPUT = (By.CSS_SELECTOR, '[data-testid="password-input"]')
SUBMIT_BUTTON = (By.CSS_SELECTOR, '[data-testid="login-btn"]')
ERROR_MESSAGE = (By.CSS_SELECTOR, '[data-testid="error-message"]')
FORGOT_LINK = (By.LINK_TEXT, "Forgot Password")
def navigate(self):
self.navigate_to("/login")
return self # For method chaining
def enter_email(self, email):
self.type(self.EMAIL_INPUT, email)
return self
def enter_password(self, password):
self.type(self.PASSWORD_INPUT, password)
return self
def submit(self):
self.click(self.SUBMIT_BUTTON)
return self
def login(self, email, password):
"""Complete login flow in one method"""
self.navigate().enter_email(email).enter_password(password).submit()
def get_error_message(self):
return self.get_text(self.ERROR_MESSAGE)
def is_error_visible(self):
return self.is_visible(self.ERROR_MESSAGE)
# ── test_login.py ─────────────────────────────────────────────
import pytest
from pages.login_page import LoginPage
from pages.dashboard_page import DashboardPage
class TestLogin:
def test_valid_login_redirects_to_dashboard(self, driver):
login_page = LoginPage(driver)
login_page.login("alice@test.com", "Test@1234")
dashboard = DashboardPage(driver)
assert dashboard.is_loaded(), "Dashboard should be loaded after login"
assert dashboard.get_welcome_message() == "Welcome, Alice"
def test_invalid_login_shows_error(self, driver):
login_page = LoginPage(driver)
login_page.login("wrong@test.com", "wrongpass")
assert login_page.is_error_visible(), "Error message should appear"
assert "Invalid" in login_page.get_error_message()Common Mistakes
- Putting assertions in page objects — page objects contain interactions only; assertions belong in test files (separation of concerns)
- One giant page object for the whole app — create one page class per page/major component, not one class for everything
- Hardcoding waits inside page objects — use BasePage's wait utilities consistently; don't add time.sleep() in page methods
- Not returning self for chaining — returning self from action methods enables fluent chaining: loginPage.navigate().enterEmail('a').submit()
Tip
Tip
Practice Page Object Model POM Design in small, isolated examples before integrating into larger projects. Breaking concepts into small experiments builds genuine understanding faster than reading alone.
Playwright rising fast — modern API, auto-waits, all browsers
Practice Task
Note
Practice Task — (1) Write a working example of Page Object Model POM Design from scratch without looking at notes. (2) Modify it to handle an edge case (empty input, null value, or error state). (3) Share your solution in the Priygop community for feedback.
Quick Quiz
Common Mistake
Warning
A common mistake with Page Object Model POM Design is skipping edge case testing — empty inputs, null values, and unexpected data types. Always validate boundary conditions to write robust, production-ready software testing code.
Key Takeaways
- The Page Object Model (POM) is the most widely adopted design pattern in test automation.
- Problem without POM: 50 test functions all find the login button with 'input[type=submit]'. The dev renames it to 'button[data-testid=login]'. You must update 50 tests.
- Solution with POM: One LoginPage class holds the locator. 50 test functions call loginPage.clickSubmit(). You update ONE line in the class.
- Readability: Tests read like specifications — loginPage.enterEmail('alice@test.com'); loginPage.submit(); dashboardPage.assertWelcomeMessage('Alice');