Why Page Object Model? The Problem It Solves and the Architecture It Creates

Imagine you have 200 Selenium tests that all interact with the login page. Each test contains the same three locators โ€” username field, password field, login button โ€” hardcoded directly in the test method. Now a developer changes the username field’s ID from user-name to email. You must find and update that locator in all 200 files. Miss one, and a test breaks. This maintenance nightmare is exactly what the Page Object Model solves. POM is the most important design pattern in UI automation โ€” the line between amateur scripts and professional frameworks.

The Problem: Duplicated Locators and Fragile Tests

Without POM, locators and Selenium API calls are scattered across every test file. When the UI changes, the same fix must be applied in dozens of places. POM eliminates this duplication by centralising page knowledge in dedicated classes.

# โ”€โ”€ WITHOUT POM โ€” the maintenance nightmare โ”€โ”€

# test_login_valid.py
def test_valid_login(browser):
    browser.get("https://www.saucedemo.com")
    browser.find_element(By.ID, "user-name").send_keys("standard_user")  # locator here
    browser.find_element(By.ID, "password").send_keys("secret_sauce")    # locator here
    browser.find_element(By.ID, "login-button").click()                  # locator here
    assert "inventory" in browser.current_url

# test_login_invalid.py โ€” SAME locators duplicated
def test_invalid_login(browser):
    browser.get("https://www.saucedemo.com")
    browser.find_element(By.ID, "user-name").send_keys("wrong_user")     # duplicated!
    browser.find_element(By.ID, "password").send_keys("wrong_pass")      # duplicated!
    browser.find_element(By.ID, "login-button").click()                  # duplicated!
    error = browser.find_element(By.CSS_SELECTOR, "[data-test='error']")
    assert "do not match" in error.text.lower()

# test_login_empty.py โ€” SAME locators duplicated AGAIN
def test_empty_login(browser):
    browser.get("https://www.saucedemo.com")
    browser.find_element(By.ID, "login-button").click()                  # duplicated!
    error = browser.find_element(By.CSS_SELECTOR, "[data-test='error']")
    assert "username is required" in error.text.lower()

# Problem: "user-name" appears in 200 test files.
# If the ID changes to "email", you must update ALL 200 files.


# โ”€โ”€ WITH POM โ€” single source of truth โ”€โ”€

# pages/login_page.py โ€” locators defined ONCE
class LoginPage:
    URL = "https://www.saucedemo.com"
    USERNAME = (By.ID, "user-name")          # defined ONCE
    PASSWORD = (By.ID, "password")           # defined ONCE
    LOGIN_BTN = (By.ID, "login-button")      # defined ONCE
    ERROR_MSG = (By.CSS_SELECTOR, "[data-test='error']")

    def __init__(self, driver):
        self.driver = driver

    def open(self):
        self.driver.get(self.URL)
        return self

    def login(self, username, password):
        self.driver.find_element(*self.USERNAME).send_keys(username)
        self.driver.find_element(*self.PASSWORD).send_keys(password)
        self.driver.find_element(*self.LOGIN_BTN).click()

    def get_error_message(self):
        return self.driver.find_element(*self.ERROR_MSG).text

# If "user-name" changes to "email", update ONE line in LoginPage.
# All 200 tests continue to work without modification.
print("POM: Change locator in 1 place โ†’ 200 tests stay green")
Note: The Page Object Model follows the software engineering principle of separation of concerns. Page objects know how to interact with the page (locators, clicks, waits). Tests know what to verify (business rules, expected outcomes). Neither contains the other’s knowledge. This separation means UI changes only affect page objects, and business rule changes only affect tests. Without this separation, every change โ€” UI or business โ€” potentially breaks every file.
Tip: Store locators as class-level tuples โ€” USERNAME = (By.ID, "user-name") โ€” and unpack them with the asterisk operator: self.driver.find_element(*self.USERNAME). This pattern keeps locator definitions clean, makes them easy to scan visually at the top of the class, and allows you to pass them to WebDriverWait expected conditions directly: WebDriverWait(driver, 10).until(EC.visibility_of_element_located(self.USERNAME)).
Warning: POM is not just “moving locators to a separate file.” A proper page object encapsulates actions as methods (login, add_to_cart, search), not just locator constants. If your page object class is nothing but a dictionary of locators and your tests still call raw find_element and click, you have not implemented POM โ€” you have created a locator repository with the same fragility problems in a different location.

Common Mistakes

Mistake 1 โ€” Storing locators in page objects but not encapsulating actions

โŒ Wrong: Page object has locators only; test still calls login_page.driver.find_element(*LoginPage.USERNAME).send_keys("user").

โœ… Correct: Page object has a login(username, password) method that encapsulates all three steps. Test calls login_page.login("user", "pass") โ€” one readable line.

Mistake 2 โ€” Creating one giant page object for the entire application

โŒ Wrong: A single AppPage class with 500 locators and 200 methods covering every page in the application.

โœ… Correct: One page object per page or major component: LoginPage, InventoryPage, CartPage, CheckoutPage. Each class has 5โ€“20 locators and 5โ€“15 methods. Keep classes focused and manageable.

🧠 Test Yourself

What is the primary benefit of the Page Object Model in Selenium automation?