Skip to content
CodeKruPro

Framework · Python

Page objects and design patterns

The Page Object Model, page factories and patterns that keep tests tidy.

Learn: Page objects and design patterns in a Selenium Python framework9 minute read

The Page Object Model (POM) is a way to organize UI test code: every page of the app, or every reusable part of a page, gets its own class. "Explain the Page Object Model" is one of the most frequent framework questions. In Python the pattern is light: a class per page, locators stored as tuples, and small methods. Fixtures handle the set-up that page objects used to carry.

The problem

A locator is the instruction that tells Selenium how to find an element on the page, for example (By.ID, "username") means "the element whose id is username". Imagine fifty tests that each call driver.find_element(By.ID, "username"). A developer renames the field, and fifty tests fail. You edit fifty files. With page objects, that locator lives in one class, so you change one line.

A page object

The class keeps the page's locators and offers methods for what a user can do there. Tests call the methods and keep the assertions (the checks that fail a test). Waits live in a base page that every page inherits from; a wait tells Selenium to pause until an element is ready, so tests need no fixed sleep.

from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC


class BasePage:
    def __init__(self, driver, timeout=10):
        self.driver = driver
        self.wait = WebDriverWait(driver, timeout)

    def visible(self, locator):
        return self.wait.until(EC.visibility_of_element_located(locator))

    def click(self, locator):
        self.wait.until(EC.element_to_be_clickable(locator)).click()

    def type(self, locator, text):
        field = self.visible(locator)
        field.clear()
        field.send_keys(text)


class LoginPage(BasePage):
    USER = (By.ID, "username")
    PASSWORD = (By.ID, "password")
    SUBMIT = (By.CSS_SELECTOR, "button[type='submit']")
    ERROR = (By.CSS_SELECTOR, ".login-error")

    def login_as(self, name, password):
        self.type(self.USER, name)
        self.type(self.PASSWORD, password)
        self.click(self.SUBMIT)
        return DashboardPage(self.driver)

    def error_text(self):
        return self.visible(self.ERROR).text

Each locator is a tuple (a fixed pair of values written in brackets): how to find the element and what to look for. Selenium's waits take that tuple directly, and find_element(*self.USER) unpacks it (the * spreads the pair out) into the two arguments find_element needs.

Rules that keep page objects healthy

  • No assertions inside. Return values and let the test assert, so one page serves positive and negative tests.
  • Return the next page, so flows read like journeys: LoginPage(driver).login_as(...).open_orders().
  • Keep locators and find elements when a method runs. A stored element can go stale (the page was redrawn, so the stored element no longer points to anything on the screen); a locator cannot.
  • Give widgets used on many pages, such as a header or a data table, their own component class.

Page objects and fixtures together

Fixtures decide the starting state; page objects describe the screen. A fixture can log in through the API, open the right URL and hand the test a ready page:

@pytest.fixture
def dashboard(driver, api_token, config):
    driver.get(config["base_url"])
    driver.add_cookie({"name": "session", "value": api_token})
    driver.get(config["base_url"] + "/dashboard")
    return DashboardPage(driver)

def test_shows_orders(dashboard):
    assert dashboard.order_count() > 0

So the modern answer to "page objects or fixtures?" is both: fixtures for state, page objects for talking to the screen.

Page Factory

Interviewers sometimes ask about Page Factory and @FindBy. These belong to Selenium's Java support library; Selenium Python has no Page Factory. Python page objects get the same benefit from class-level locator tuples. If you like the field style, a property per element works too (@property lets you write page.submit_button without brackets):

@property
def submit_button(self):
    return self.driver.find_element(*self.SUBMIT)

A property finds the element again on every access, much like the proxies Java's Page Factory creates.

Object repository

An object repository is one central place for locators. In a code-based framework the page classes are that place. Some teams keep locators in YAML or JSON so that people who do not code can edit them, but then the editor cannot help you find where a locator is used.

Design patterns in Python frameworks

  • Factory (one function that creates objects for you): create_driver(browser, headless, grid_url) returns a local or remote driver, and the driver fixture calls it.
  • Singleton (only one object exists), the pytest way: session-scoped fixtures for configuration and tokens instead of module-level globals (variables at the top of a file that any test can change). They stay safe with pytest-xdist, the plugin that runs tests in parallel, where each worker process gets its own (see Parallel and CI runs).
  • Builder or data classes (a builder creates an object step by step; a data class mostly just holds fields): @dataclass test data with defaults, so a test sets only what matters.
  • Fluent page methods (calls that chain one after another) that return the next page.

Framework types

Frameworks are usually called data-driven, keyword-driven, BDD or hybrid. Data-driven means the same steps run with external data, keyword-driven means steps are written as keywords in tables, and BDD means scenarios in plain English (see BDD with Cucumber or behave). A pytest framework with page objects and parametrized data is a hybrid, which is what most teams run.

How to answer in an interview

Define POM in one sentence, give the benefit (one place to fix a locator, readable tests), and show a method that returns the next page. If asked about Page Factory, say it is a Java helper, that Python uses locator tuples instead, and that fixtures now do the set-up work.

Common mistakes

  • Asserting inside page methods.
  • Storing elements in __init__, where they go stale after the page changes.
  • Claiming Selenium Python has @FindBy.
  • Keeping the driver in a module-level global instead of a fixture.

Recap

A page object keeps one page's locators and user actions in one class, and tests keep the assertions. Locators are tuples, waits go in a base page, and methods return the next page. Fixtures build the starting state, and session fixtures replace global singletons.

9 questions0/9 solved

StatusQuestionDifficultyAsked in
Not startedIn the Page Object Model, where do assertions belong?Easy-MediumAsked in: Accenture, Infosys, TCS +1
Not startedWhat is the Page Object Model and why use it? (Python)Easy-MediumAsked in: Accenture, Infosys, TCS +1
Not startedBuild a small page object in PythonMediumAsked in: Accenture, Infosys, TCS +1
Pro+Is there a Page Factory in Selenium Python?MediumAsked in: Infosys, TCS, Wipro
Pro+Design patterns in your framework (Python)Medium-HardAsked in: Infosys
Pro+What should loginAs() return in a page object?MediumAsked in: Infosys
Pro+Framework types: data-driven, keyword-driven, hybrid and BDDMediumAsked in: Accenture, Capgemini, Infosys +1
Pro+What is an object repository?EasyAsked in: Accenture, TCS
Pro+Should you still use page objects, or fixtures and helpers instead?Medium-Hard
Not startedNot startedIn progressIn progressSolvedSolvedPro+Pro+

The first 2 questions of each topic are free. Pro+ opens every question.

Back to the plan