munotes®

The Page Object Model

Get access to whole semester resourcesSemester Pass

Chapter Fifty-One

Syllabus topic Practical, "Page Object Model (POM) Framework Implementation"

Pages 278 to 282 of 622

In one line

A page object is a class that represents a page, or a part of one, and offers the actions a user can take there as methods named for what they do; tests call those methods and never touch the page's HTML, so when the page changes, only the page object changes.

In the wording a student can write in an examination: the Page Object Model (POM) is "a Design Pattern that has become popular in test automation for enhancing test maintenance and reducing code duplication. A page object is an object-oriented class that serves as an interface to a page of your AUT" (application under test) (Selenium documentation). In Martin Fowler's words, "A page object wraps an HTML page, or fragment, with an application-specific API, allowing you to manipulate page elements without digging around in the HTML." Its advantages are "a clean separation between the test code and page-specific code, such as locators" and "a single repository for the services or operations the page offers". Page objects expose services, not internals; they generally make no assertions, which belong in the tests; and their methods return other page objects, so a test reads as a user's journey.

The problem it solves

Chapter Forty-Eight showed what brittle scripts cost: automation that needs repair after every change may never pay for itself. The commonest cause of that brittleness is the one Fowler names: "if you write tests that manipulate the HTML elements directly your tests will be brittle to changes in the UI." When every test that logs in contains the login page's three locators, a redesign of the login page breaks every one of those tests, and each must be found and repaired.

The Selenium documentation states the remedy's benefit in a sentence: "if the UI changes for the page, the tests themselves don't need to change, only the code within the page object needs to change. Subsequently, all changes to support that new UI are located in one place."

What a page object looks like

Fowler's rule of thumb is that a page object "should allow a software client to do anything and see anything that a human can", through "an interface that's easy to program to and hides the underlying widgetry in the window." His guidance on the interface is concrete: "to access a text field you should have accessor methods that take and return a string, check boxes should use booleans, and buttons should be represented by action oriented method names." And the test of a good one: "A good rule of thumb is to imagine changing the concrete control - in which case the page object interface shouldn't change."

The Selenium documentation summarises the rules:

munotes.in278

The Page Object Model

  • "The public methods represent the services that the page or component offers"
  • "Try not to expose the internals of the page or component"
  • "Generally don't make assertions"
  • "Methods return other Page Objects, Page Component Objects, or optionally themselves (for fluent syntax)"
  • "Need not represent an entire page all the time"
  • "Different results for the same action are modelled as different methods"

Two of these deserve a word. On assertions, the documentation is firm: "Page objects themselves should never make verifications or assertions. This is part of your test and should always be within the test's code". Fowler agrees ("I favor having no assertions in page objects"), allowing only checks of a page's invariants, such as that the browser is on the right page when the page object is created. On size, "a Page Object need not represent an entire page": a navigation bar that appears everywhere can be one component object, and "The essential principle is that there is only one place in your test suite with knowledge of the structure of the HTML of a particular (part of a) page."

Worked example: a redesign of the login page

ExamReg's login page is redesigned in release 2: its three fields and button get new ids. The program runs the same three login tests written two ways, first with the locators inside every test, then through a LoginPage and a HomePage page object, against both releases. A simulated browser stands in for the real one, keeping WebDriver's method names, so the program runs anywhere.

class NoSuchElement(Exception):
    pass

class FakeDriver:                             # a browser, simulated so the tests run anywhere
    def __init__(self, login_ids):
        self.login_ids, self.page, self.typed = login_ids, "login", {}
    def find_element(self, by, value):
        on_page = self.login_ids.values() if self.page == "login" else ["welcome"]
        if value not in on_page:
            raise NoSuchElement(value)
        return FakeElement(self, value)

class FakeElement:
    def __init__(self, driver, element_id):
        self.driver, self.id = driver, element_id
    def send_keys(self, text):
        self.driver.typed[self.id] = text
    def click(self):
        ids, typed = self.driver.login_ids, self.driver.typed
        if typed.get(ids["roll"]) == "2026CS014" and typed.get(ids["password"]) == "test-pass-1":
            self.driver.page = "home"
    @property
    def text(self):
        return "Welcome, 2026CS014" if self.id == "welcome" else ""

RELEASE_1 = {"roll": "roll", "password": "pwd", "button": "login-btn"}
RELEASE_2 = {"roll": "roll-number", "password": "password", "button": "sign-in"}   # a redesign

# --- tests WITHOUT page objects: every test knows the page's HTML ---------------------------
def raw_login_succeeds(driver):
    driver.find_element("id", "roll").send_keys("2026CS014")
    driver.find_element("id", "pwd").send_keys("test-pass-1")
    driver.find_element("id", "login-btn").click()
    return "2026CS014" in driver.find_element("id", "welcome").text

def raw_wrong_password_refused(driver):
    driver.find_element("id", "roll").send_keys("2026CS014")
    driver.find_element("id", "pwd").send_keys("wrong")
    driver.find_element("id", "login-btn").click()
    return driver.page == "login"          # stands for checking the page address

def raw_empty_roll_refused(driver):
    driver.find_element("id", "roll").send_keys("")
    driver.find_element("id", "pwd").send_keys("test-pass-1")
    driver.find_element("id", "login-btn").click()
    return driver.page == "login"          # stands for checking the page address

# --- the same tests WITH page objects: only the page classes know the HTML --------------------
class LoginPage:
    ROLL, PASSWORD, BUTTON = ("id", "roll"), ("id", "pwd"), ("id", "login-btn")
    def __init__(self, driver):
        self.driver = driver
    def log_in_as(self, roll_no, password):   # a service the page offers, named by intention
        self.driver.find_element(*self.ROLL).send_keys(roll_no)
        self.driver.find_element(*self.PASSWORD).send_keys(password)
        self.driver.find_element(*self.BUTTON).click()
        return HomePage(self.driver) if self.driver.page == "home" else self

class HomePage:
    def __init__(self, driver):
        self.driver = driver
    def welcome_text(self):
        return self.driver.find_element("id", "welcome").text

def pom_login_succeeds(driver):
    home = LoginPage(driver).log_in_as("2026CS014", "test-pass-1")
    return isinstance(home, HomePage) and "2026CS014" in home.welcome_text()

def pom_wrong_password_refused(driver):
    return isinstance(LoginPage(driver).log_in_as("2026CS014", "wrong"), LoginPage)

def pom_empty_roll_refused(driver):
    return isinstance(LoginPage(driver).log_in_as("", "test-pass-1"), LoginPage)

def run(tests, release):
    passed = 0
    for test in tests:
        try:
            passed += test(FakeDriver(release))
        except NoSuchElement:
            pass                               # a locator no longer matches the page
    return f"{passed} of {len(tests)} pass"

RAW = [raw_login_succeeds, raw_wrong_password_refused, raw_empty_roll_refused]
POM = [pom_login_succeeds, pom_wrong_password_refused, pom_empty_roll_refused]
print("release 1, without page objects:", run(RAW, RELEASE_1))
print("release 1, with page objects:   ", run(POM, RELEASE_1))
print("release 2, without page objects:", run(RAW, RELEASE_2))
print("release 2, with page objects:   ", run(POM, RELEASE_2))
LoginPage.ROLL, LoginPage.PASSWORD, LoginPage.BUTTON = (
    ("id", "roll-number"), ("id", "password"), ("id", "sign-in"))   # the one repair
print("release 2, after repairing LoginPage only:", run(POM, RELEASE_2))
munotes.in279

The Page Object Model

release 1, without page objects: 3 of 3 pass
release 1, with page objects:    3 of 3 pass
release 2, without page objects: 0 of 3 pass
release 2, with page objects:    0 of 3 pass
release 2, after repairing LoginPage only: 3 of 3 pass

On release 1 both styles pass, and nothing yet shows the difference. On release 2 both fail, as they must: the page really changed. The difference is in the repair. The tests without page objects contain the login page's three locators nine times, three in each test, and every occurrence must be found and changed; in a real suite, with dozens of tests that log in, the count runs into the hundreds. The page-object tests contain them three times, in LoginPage alone, and the one repair the program makes, three lines in one class, brings all three tests back. The tests themselves were not touched; they still read as intentions: log in as this student, and check the welcome.

Notice also the page objects' shape, which follows the rules above. log_in_as is a service named for what a user does, not for which boxes are filled; it returns a HomePage when the login succeeds and the LoginPage itself when it does not, which the documentation's rule "Different results for the same action are modelled as different methods" would split into two methods in a larger suite. And neither page object makes an assertion: HomePage.welcome_text returns the text, and the test decides whether it is right.

POM with TestNG, in the practical

The practical asks students to "Design and implement automation framework using Page Object Model (POM). Create reusable page classes and execute modular test cases." The Java shape is the same as the Python one: a class for each page or component, its locators as private fields, its services as public methods returning page objects; and TestNG test classes (Chapter Thirty-Five, on writing unit tests with a framework) that create the page objects in a @BeforeMethod fixture and make their assertions in @Test methods. Data-driven tests (Chapter Fifty) combine naturally with it: the data provider supplies the rows, and the page objects carry them through the pages.

munotes.in280

The Page Object Model

What it does not mean

A page object is not one class per web page. Fowler notes the name misleads: page objects should be built "for the significant elements on a page", so a page may have several.

A page object is not a place for assertions. It provides access to the page; the test decides what is correct.

POM does not make tests immune to change. When the page changes, the page object must still be repaired; the gain is that the repair happens once.

POM is not only for Selenium. Any test that drives a user interface, a mobile app's screens included, can hide the interface's details behind objects named for what users do.

Quick revision

  • POM (Selenium documentation): a design pattern for test maintenance and less duplication; a page object is a class serving as the interface to a page of the application under test.
  • Fowler: a page object "wraps an HTML page, or fragment, with an application-specific API"; it should let a client "do anything and see anything that a human can"; test it by imagining the concrete control changed.
  • Advantages: separation of test code from page-specific code such as locators; a single repository of the page's services; UI changes repaired in one place.
  • Rules: public methods are services; hide internals; generally no assertions; return other page objects; need not be a whole page; different results, different methods.
  • Worked example: after a redesign, nine inline locators across three tests against three in one page object; one repair restored all three page-object tests.

Test yourself

1. What is the Page Object Model, and what problem does it solve? A design pattern in which each page, or significant part of one, is represented by a class that offers the user's actions as methods and hides its locators and layout. It solves the brittleness of tests that manipulate HTML directly: when a page changes, only its page object needs repairing, not every test that uses the page.

2. State two advantages of POM as the Selenium documentation gives them. A clean separation between test code and page-specific code such as locators and layout; and a single repository for the services or operations the page offers, instead of having them scattered through the tests. Both mean that changes for a new UI are made in one place.

munotes.in281

The Page Object Model

3. Should a page object contain assertions? Explain. Generally no. Assertions are part of the test and belong in the test's code; the page object provides access to the page's data and services. The accepted exception is a check of the page's invariants, such as confirming the browser is on the expected page when the object is created.

4. What should a page object's methods return, and why? Other page objects, component objects, or the page object itself, so that a test can follow the user's journey from page to page; results such as text or states are returned as fundamental types for the test to assert on.

5. In the worked example, why did the page-object tests need only one repair after the redesign? Because the login page's locators appeared only in the LoginPage class; the tests called its log_in_as method and never named an element, so changing the three locators in that one class repaired every test that logs in.

munotes.in282

The rest of this subject

These notes are cut from the University's printed syllabus. Open the syllabus itself, or the past papers, for the same subject.

Issue
Done!