Fun with Learning Technology
LearnCoursesQuestionsTracksToolsNewsExplorePractice
Fun with Learning Technology

A new problem, explained clearly, every day.

Subscribe
Learn
  • Lessons
  • Topics
  • News
  • Tools
  • Courses
  • Career tracks
  • Everything
Site
  • About
  • Contact
  • Support
  • Privacy
  • Terms
Get the daily one

One email per new problem. No spam.

Request a tutorial

Requests shape what gets made next.

© 2026 Fun with Learning TechnologyRSS
Home›Courses›Selenium›Page Object Model (POM) Pattern

Advanced Topics

Page Object Model (POM) Pattern

The Page Object Model is a structural design pattern that creates an object-oriented layer between your test scripts and the browser's user interface. By encapsulating UI elements and their interactions into dedicated classes, it transforms fragile test code into a maintainable and reusable framework. You should adopt this pattern as soon as your test suite grows beyond simple smoke tests to prevent the widespread maintenance burden caused by frequent application UI changes.

The Core Concept of Encapsulation

The fundamental problem with writing direct selector-based tests is the tight coupling between the test logic and the HTML structure. When a developer changes a button ID or a CSS class name, every test script referencing that element will fail simultaneously, necessitating a painful, manual update across dozens of files. The Page Object Model addresses this by isolating selectors within class methods. Instead of interacting with the browser directly inside the test, you interact with a Page Object that represents the application's view. This layer of abstraction ensures that your test scripts focus solely on user behavior and business logic rather than the low-level implementation details of the DOM. By treating a page as an object, you create a central repository for selectors, ensuring that if a UI component changes, you only need to update the selector in one specific class method, automatically fixing all tests that utilize it.

class LoginPage:
    # Encapsulate locators to prevent repetition
    USERNAME_FIELD = "#user-name"
    PASSWORD_FIELD = "#password"
    LOGIN_BUTTON = "#login-button"

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

    def enter_username(self, username):
        self.driver.find_element("css selector", self.USERNAME_FIELD).send_keys(username)

Implementing Action Methods

Beyond storing identifiers, a robust Page Object should expose high-level action methods that mirror user tasks. A common mistake is to expose raw WebDriver calls to the test script, which forces the script to manage timing, interaction sequencing, and data entry. Instead, a Page Object should handle the 'how' while the test script handles the 'what.' For instance, a login method in a Page Object should take credentials as parameters, locate the necessary fields, perform the interaction, and potentially return a new Page Object representing the next state of the application. This approach reduces code duplication significantly because complex interactions, such as filling out a multi-step form or navigating a complex menu structure, are written once and reused across many test scenarios. When you design methods to return other Page Objects, you create a readable, chainable flow that mimics the actual movement of a user navigating through the application features.

class LoginPage:
    # ... (previous code)
    def login(self, username, password):
        self.driver.find_element("css selector", self.USERNAME_FIELD).send_keys(username)
        self.driver.find_element("css selector", self.PASSWORD_FIELD).send_keys(password)
        self.driver.find_element("css selector", self.LOGIN_BUTTON).click()
        # Return instance of the next page to enable chaining
        return DashboardPage(self.driver)

Handling Asynchronous UI States

One of the most critical challenges in web automation is dealing with dynamic content that loads asynchronously. If a test script attempts to interact with an element before it has appeared in the DOM, the test will crash. Page Objects provide the perfect location to implement explicit waits that ensure the browser is in the correct state before proceeding. By embedding these waits within the action methods of the Page Object, you ensure that every test inheriting from that object automatically benefits from robust synchronization. This creates a self-healing layer where the test script doesn't need to know about the performance characteristics of specific UI elements; it simply calls the method and the Page Object manages the necessary timing. This pattern prevents the common 'flaky test' syndrome where tests pass inconsistently based on network latency, as the logic for ensuring UI readiness is permanently coupled with the element interaction itself.

from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

class DashboardPage:
    LOGOUT_BTN = "#logout"
    def wait_for_load(self):
        # Centralize wait logic within the page object
        wait = WebDriverWait(self.driver, 10)
        wait.until(EC.visibility_of_element_located(("css selector", self.LOGOUT_BTN)))

Structuring for Scalability

As your project expands to include dozens of pages, managing individual files can become difficult if you do not implement a hierarchical structure. You should organize your Page Objects to mirror the physical layout of your application. Often, it is beneficial to create a BasePage class that holds shared functionality, such as navigating to a URL, common header interactions, or global wait methods that every other page needs. This inheritance hierarchy prevents the repetition of standard browser management code across every page definition. Furthermore, this structure allows you to enforce standard naming conventions for elements and methods, ensuring that any new team member can easily navigate the framework. By thinking of the framework as a collection of modular components rather than a loose collection of files, you transform your test suite into a maintainable asset that scales alongside the complexity of the application under test.

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

    def get_title(self):
        return self.driver.title

class LoginPage(BasePage):
    # LoginPage inherits shared BasePage capabilities
    pass

Decoupling Logic from Data

Finally, the Page Object Model facilitates the total decoupling of test data from test behavior. Because the Page Objects act as the bridge, your test scripts only ever pass in parameters—such as usernames, passwords, or search queries—without caring how they are processed. This allows you to pull test data from external sources like JSON files, databases, or CSVs without ever modifying the Page Object code itself. Your test scripts essentially become orchestrators that feed data into Page Objects and verify the expected outcomes. This separation is vital for modern testing workflows, as it enables the execution of the same test scenarios across multiple data sets. When you follow this final layer of the pattern, your test suite becomes highly resilient, easily readable, and profoundly simple to expand, as each new requirement simply becomes a new method in a Page Object rather than a complete rewrite of a functional test script.

def test_login_flow(driver):
    # The test script handles the data and the flow
    user_data = {"user": "test_admin", "pass": "secret123"}
    login_page = LoginPage(driver)
    dashboard = login_page.login(user_data['user'], user_data['pass'])
    assert "Dashboard" in dashboard.get_title()

Key points

  • Page objects wrap the UI details to hide them from the test execution layer.
  • Centralizing locators ensures that UI changes require updates in exactly one place.
  • Page objects should return subsequent page instances to facilitate fluent test chaining.
  • Embedding explicit waits within page methods prevents intermittent test failures due to network latency.
  • A BasePage class serves as the foundation for common driver operations and utilities.
  • Action methods in page objects should describe business tasks rather than technical interactions.
  • Separating data from test scripts allows for running identical tests with varied input parameters.
  • The POM pattern promotes a modular architecture that improves long-term maintainability for large suites.

Common mistakes

  • Mistake: Including assertions inside Page Object classes. Why it's wrong: Page Objects should represent the UI structure and provide services, not dictate test logic or validation. Fix: Perform all assertions within the Test script classes.
  • Mistake: Writing raw Selenium driver commands directly in test methods. Why it's wrong: This couples tests to the HTML structure, leading to high maintenance costs when elements change. Fix: Encapsulate all driver interactions within page methods.
  • Mistake: Hardcoding explicit wait times (Thread.sleep) inside page methods. Why it's wrong: It makes tests brittle, slow, and unreliable due to varying network speeds. Fix: Use WebDriverWait to poll for element presence or visibility.
  • Mistake: Creating a single 'God' Page Object for the entire application. Why it's wrong: It becomes too large, hard to read, and difficult to debug. Fix: Break pages into smaller, reusable classes following the actual modularity of the UI.
  • Mistake: Exposing internal web elements directly as public members. Why it's wrong: It breaks encapsulation and allows tests to interact with the DOM in ways that bypass the defined Page Object workflows. Fix: Keep element locators private and provide public methods for user interactions.

Interview questions

What is the Page Object Model in Selenium and why do we use it?

The Page Object Model, or POM, is a design pattern in Selenium where each web page is represented as a corresponding class. Instead of cluttering test scripts with raw locators, we encapsulate these locators and the actions performed on them within the Page Object class. We use it primarily to improve test maintainability and code reusability. By centralizing the element identification logic, if a UI element changes, we only need to update it in one place rather than searching through hundreds of individual test cases, which significantly reduces the technical debt of our automation framework.

How do you implement Page Objects in a Selenium framework?

To implement POM in Selenium, you create a class for each page in your application. Within that class, you declare your locators as private variables, typically using @FindBy annotations or by defining By objects. You then write public methods to represent the operations a user can perform, such as 'login()' or 'submitForm()'. In your test script, you instantiate the Page Object class and call these methods. For example: 'loginPage.enterUsername(user); loginPage.clickLogin();'. This abstraction layer ensures that the test script focuses on the business logic rather than the underlying DOM structure.

What is the role of the PageFactory class in Selenium and how does it relate to POM?

PageFactory is a built-in Selenium utility designed to support the Page Object Model. It provides the @FindBy annotation, which allows for cleaner and more readable locator definitions. The core functionality is 'lazy initialization,' meaning elements are not located until the moment they are actually interacted with during the test execution. When you call 'PageFactory.initElements(driver, this)', Selenium initializes the annotated elements. It simplifies the setup process, as you no longer need to manually find elements using the driver object within the constructor, making the code much more concise and easier to read.

Compare the Page Object Model to the traditional approach of embedding locators directly in test scripts.

The traditional approach, often called 'scripting,' embeds locators directly inside the test methods. While this might seem faster for simple projects, it becomes a nightmare to maintain as the application grows. If a button's ID changes, you must find and update every single test case that uses that button. In contrast, the Page Object Model decouples the test logic from the page structure. POM provides a single source of truth for elements, meaning a single change in the Page Object class automatically propagates to all tests. POM essentially turns fragile, brittle scripts into a robust, object-oriented suite that scales efficiently alongside the application's development.

How should you handle dynamic elements or wait conditions when using the Page Object Model?

Handling dynamic elements within POM should be done by incorporating Explicit Waits directly into the Page Object methods. Instead of using thread sleeps, you should use the WebDriverWait class combined with ExpectedConditions. By encapsulating these waits inside the page methods—for example, having a 'clickSubmit()' method that first waits for the element to be clickable—you ensure that the test is resilient to slow network speeds or asynchronous UI rendering. This keeps the test scripts clean, as the test doesn't need to worry about synchronization; the Page Object handles the timing logic internally, ensuring the page is ready before acting.

How can you architect a complex Page Object Model using the Fluent Interface design pattern?

To implement a Fluent Interface in POM, you design your page methods to return the instance of the next page object or the current page object instead of returning 'void'. For example, after a 'login()' method finishes, it can return 'new DashboardPage(driver)'. This allows you to chain methods together in your test script like this: 'loginPage.enterCredentials().clickLogin().verifyDashboardHeader()'. This approach makes tests read like human-friendly sentences, significantly increasing readability. It also enforces a logical navigation flow through the application, preventing developers from attempting to perform actions on a page that hasn't been navigated to or rendered yet, thus improving both code organization and test reliability.

All Selenium interview questions →

Check yourself

1. When a developer updates the ID of a 'Login' button, what is the primary benefit of having implemented the Page Object Model?

  • A.The test execution speed automatically increases.
  • B.Only the specific Page Object class needs to be modified, leaving test scripts untouched.
  • C.Selenium automatically detects the locator change during runtime.
  • D.The browser driver will automatically update the locator in the DOM.
Show answer

B. Only the specific Page Object class needs to be modified, leaving test scripts untouched.
Option 1 is correct because POM encapsulates locators, localizing maintenance. Option 0 is irrelevant to structure; Option 2 is false as Selenium cannot self-heal locators without custom wrappers; Option 3 is false as DOM is controlled by the app, not the driver.

2. Why is it considered a bad practice to perform assertions inside your Page Object methods?

  • A.It prevents the use of PageFactory for initialization.
  • B.It makes the Page Object class too dependent on the test execution environment.
  • C.It violates the principle that Page Objects should represent the page structure, not define test success criteria.
  • D.Selenium does not support the Assert class within page classes.
Show answer

C. It violates the principle that Page Objects should represent the page structure, not define test success criteria.
Option 2 is correct because Page Objects model the UI; test assertions define intent. Option 0 is incorrect; Option 1 is vague; Option 3 is incorrect as the framework doesn't restrict it, but logic does.

3. Which of the following describes the ideal interaction between a Test Script and a Page Object?

  • A.The Test Script directly calls driver.findElement() using locators defined in the test file.
  • B.The Test Script invokes high-level service methods provided by the Page Object.
  • C.The Test Script accesses private WebElement variables directly from the Page Object.
  • D.The Test Script overrides the Page Object's constructor to inject custom driver settings.
Show answer

B. The Test Script invokes high-level service methods provided by the Page Object.
Option 1 is correct because it encapsulates behavior into reusable methods. Option 0 bypasses POM; Option 2 breaks encapsulation; Option 3 is a misuse of design patterns.

4. In a robust implementation, how should a Page Object handle a situation where a user must wait for an element to load after a button click?

  • A.Use a static sleep command to ensure the network has time to respond.
  • B.Expect the test script to handle the wait before calling the next Page Object method.
  • C.Implement explicit waits inside the method that performs the interaction.
  • D.Rely on the default implicit wait set at the driver level for all interactions.
Show answer

C. Implement explicit waits inside the method that performs the interaction.
Option 2 is correct as it keeps the page logic self-contained. Option 0 is poor practice; Option 1 forces test scripts to know too much about page behavior; Option 3 is less reliable than explicit waits.

5. What is the primary architectural goal of using Page Object Model in a Selenium suite?

  • A.To maximize the amount of code shared between the UI and the backend API.
  • B.To achieve a clean separation between the test logic and the application's UI implementation details.
  • C.To automatically generate HTML reports of the test failures.
  • D.To eliminate the need for using locators like XPaths or CSS Selectors.
Show answer

B. To achieve a clean separation between the test logic and the application's UI implementation details.
Option 1 is the definition of POM. Option 0 is unrelated; Option 2 is a byproduct of reporting tools; Option 3 is impossible as elements must always be located.

Take the full Selenium quiz →

← PreviousFluent WaitsNext →Taking Screenshots

Selenium

18 lessons, free to read.

All lessons →

Track your progress

Sign in to mark lessons done, score quizzes and keep notes.

Open in the app