Colibri Software

Page Object Model (POM) in Selenium
using Python

By ILLIA

Page Object Model (POM) in Selenium using Python

This article describes a design pattern called Page Object Model that is considered one of the best practices when developing end-to-end UI automated tests using Selenium. All examples are shown using Python.

What’s POM?

Page Object Model is a design pattern that aims to reduce code duplication and to minimize the effort involved in code maintenance. Under the Page Object Model, page classes are created for all the web pages that tested via end-to-end UI automated tests. A single page object is an object-oriented class that serves as an interface to a page.

This makes the code more independent since locators used by the test scenarios are stored in a separate class file, and the test cases, which contain the core test logic, are stored in a different file. Therefore, any change in the web UI elements will require minimal or no changes in the test scenarios since locators and test scripts are stored separately.

Implementation based on the Page Object Model (POM) consists of the following elements:

  • Page Object Element – The Page Class that contains the web elements of the web pages under test. It also contains an implementation of the methods to perform operations on these web elements. It is based on the fundamentals of Object-Oriented Programming.
  • Test Cases – Test cases contain the implementation of the actual test scenarios. It uses the same methods as Page class to interact with the page’s UI elements. If there is a change in the UI of the web page, only the Page Class needs to be updated, and the test code remains unchanged.

Example of a test that does not follow POM design patterns

This is a simple test case to test the “Contact me” form on the Contact page of the Colibri Software website.

import unittest

class TestColibriSoftwareContactMe(unittest.TestCase):

    def test_submit_contact_me_form(self, browser):
        """
        Tests "Contact me" form.
        Populates the fields, submits the forms and verifies the success message
        """

        URL = 'https://www.colibri-software.com/contact/'
        browser.get(URL)

        name_input = browser.find_element_by_id('wpforms-236-field_0')
        company_name_input = browser.find_element_by_id('wpforms-236-field_4')
        email_input = browser.find_element_by_id('wpforms-236-field_1')
        additional_info_input = browser.find_element_by_id('wpforms-236-field_0')
        submit_button = browser.find_element_by_css_selector('button[type="submit"]')

        name_input.send_keys('John Doe')
        company_name_input.send_keys('John Doe\'s paper company')
        email_input.send_keys('[email protected]')
        additional_info_input.send_keys('I need a website to sell paper online')
        submit_button.click()

        success_message = 'Thanks for contacting us! We will be in touch with you shortly.'
        assert success_message in browser.page_source

The issue here is that all page-related issues such as such as page URL and page elements are defined inside of the test case. If another test with a negative test scenario needs to be added, you will need to copy-paste those selectors. Eventually, you end up with many end-to-end tests. Now imagine that the selectors were changed and your test cannot find some of the elements anymore. It will take a while to go through each test and manually update the outdated selectors. That is why it is better to have a separate page class for your test cases, and if something like this happens, there will be one place to update and the test cases remain the same.

Page Object Model is a design pattern that focused on reducing code duplication and minimization of the effort involved in code maintenance.

The same test following a POM design pattern

Project structure

Shown below is a sample directory structure for using Page Objects in a Selenium test automation project.

|
├── pages
|   ├── locators.py
|   ├── elements.py
│   └── pages.py
└── tests
    └── test_contact_page.py
Test case class

This is a test case file that populates the “Contact me” form on the colibri-software.com website and asserts that the success message is displayed.

import unittest
from selenium import webdriver
import pages

class TestColibriSoftwareContactMe(unittest.TestCase):

    def setUp(self):
        self.driver = webdriver.Firefox()
        self.driver.get("https://www.colibri-software.com")

    def test_submit_contact_me_form(self):
        """
        Tests "Contact me" form.
        Populates the fields, submits the forms and verifies the success message
        """

        # Load the main page. In this case the home page of colibri-software.com.
        contact_page = pages.ContactPage(self.driver)

        # Checks if the word "Contact" is in title
        assert contact_page.is_title_matches()

        # Populating the form
        contact_page.name_input = 'John Doe'
        contact_page.company_name_input = 'John Doe\'s paper company'
        contact_page.email_input = '[email protected]'
        contact_page.additional_info_input = 'I need a website to sell paper online'

        # Submitting the form
        contact_page.click_submit_form_button()

        # Verifies that success message is displayed
        assert contact_page.success_message_is_displayed()

    def tearDown(self):
        self.driver.close()

if __name__ == "__main__":
    unittest.main()
Page object class

This section introduces the page module where the page objects are defined. The pages.py will look like this:

from elements import BasePageElement
from locators import ContactPageLocators

class NameElement(BasePageElement):
    """
    This class gets the search text from the specified locator
    """

    # The locator for text box where name is entered
    locator = 'wpforms-236-field_0'

# Similar classes for other text fields    
class CompanyNameElement(BasePageElement):

    locator = 'wpforms-236-field_4'

class EmailElement(BasePageElement):

    locator = 'wpforms-236-field_1'

class AdditionalInfoElement(BasePageElement):

    locator = 'wpforms-236-field_0'

class BasePage(object):
    """
    Base class to initialize the base page that will be called from all pages
    """

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

class ContactPage(BasePage):
    """
    Contact page action methods come here
    """

    # Declares text input fields
    name_input = NameElement()
    company_name_input = CompanyNameElement()
    email_input = EmailElement()
    additional_info_input = AdditionalInfoElement()

    def is_title_matches(self):
        """
        Verifies that the text "Contact" appears in page title
        """
        return 'Contact' in self.driver.title

    def click_submit_form_button(self):
        """
        Submits the form
        """
        element = self.driver.find_element(*ContactPageLocators.SUBMIT_FORM_BUTTON)
        element.click()

    def success_message_is_displayed(self):
        success_message = 'Thanks for contacting us! We will be in touch with you shortly.'
        return success_message in self.driver.page_source
Page elements

The elements.py will look like this:

from selenium.webdriver.support.ui import WebDriverWait

class BasePageElement(object):
    """
    Base page class that is initialized on every page object class.
    """

    def __set__(self, obj, value):
        """
        Sets the text to the value supplied
        """
        driver = obj.driver
        WebDriverWait(driver, 100).until(lambda driver: driver.find_element_by_id(self.locator))
        driver.find_element_by_id(self.locator).clear()
        driver.find_element_by_id(self.locator).send_keys(value)

    def __get__(self, obj, owner):
        """
        Gets the text of the specified object
        """
        driver = obj.driver
        WebDriverWait(driver, 100).until(lambda driver: driver.find_element_by_id(self.locator))
        element = driver.find_element_by_id(self.locator)
        return element.get_attribute("value")
Locators

The locators.py will look like this:

from selenium.webdriver.common.by import By

class ContactPageLocators(object):
    """
    A class for all Contact page locators.
    """
    SUBMIT_FORM_BUTTON = (By.CSS_SELECTOR, 'button[type="submit"]')