To improve my automation skills, I am working on a QA Automation project where I have to solve various Test Cases that test many aspects of the web-application. I have started this project with the main goal of developing better understanding of Page Object Model, Selenium and PyTest.

For the bigger projects it is very important to correctly design and structure the project itself. Very popular design pattern for this purpose is called Page Object Model. This way, I would like to sum-up some of the things I have learned about POM so far.

In the world of test automation, the Page Object Model (POM) is a popular design pattern that enhances the maintainability and reusability of test scripts. By encapsulating the web elements and related actions of a particular page into a dedicated class, the POM allows for cleaner, modular, and more efficient test code.

What is the Page Object Model?

The Page Object Model is a design pattern that advocates treating each page of a web application as a separate entity. It suggests creating a corresponding class for each page, encapsulating the web elements and actions performed on that page within the class. This approach promotes code reusability, maintainability, and readability.

Benefits of Using the Page Object Model

  1. Enhanced code maintainability: With POM, any changes made to a particular page can be updated within its corresponding class, minimizing the need for extensive modifications throughout the test suite.
  2. Improved code reusability: By encapsulating page-specific elements and actions within dedicated classes, the same code can be reused across multiple tests, reducing duplication (DRY).
  3. Better collaboration: POM allows for a clear separation of responsibilities between developers and testers. Developers focus on writing page classes, while testers utilize these classes to create test scripts.
  4. Readable and modular test code: The POM promotes a more structured and organized approach to writing test scripts, making them easier to read, understand, and maintain.

Implementing the Page Object Model in Selenium with Python

As I was researching POM, I have seen around 3-4 different styles of implementation. I still have many questions and always think about better implementation, but I assume there is never only one correct approach. It all comes down to project specifications and team decision.

or my project, I have decided to use the following implementation.

Project structure:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
└── QA Automation/
    ├── src/
    │   ├── locators/
    │   │   └── locators.py
    │   ├── pages/
    │   │   ├── basepage.py
    │   │   └── loginpage.py
    │   └── settings.py
    └── tests/
        ├── test_case_1/
        │   └── test_TC1.py
        └── conftest.py

BasePage Class - All pages will inherit from this Class

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.action_chains import ActionChains
from selenium.common.exceptions import NoSuchElementException

from src import settings

class BasePage:

    """
     Class containing shared functionality for all pages
    """

    def __init__(self, driver):
        self.driver = driver
        self._wait = WebDriverWait(self.driver, settings.WAIT_TIME)
        self._action = ActionChains(self.driver)

    def wait_for(self, locator):
        return self._wait.until(EC.presence_of_element_located(locator))

    def find_element(self, locator):

        # return self.driver.find_element(*locator)
        # Rewriting to WebDriverWait with exception handling

        try:
            return self._wait.until(EC.presence_of_element_located(locator))
        except NoSuchElementException as e:
            print(f'Element with {locator[0]} of {locator[1]} not found', e)

    def context_click(self, element):

        """
            Context click executes right click / context menu click
        """

        return self._action.context_click(element)

    def alert(self):
        return self._wait.until(EC.alert_is_present())
    
    def get_page_title(self):
        return self.driver.title

Example of a simple Login Page

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from .basepage import BasePage
from src.locators.locators import LoginPageLocators

"""
    Login Page Object Model
"""


class LoginPage(BasePage):

    def go_to_login_page(self):
        self.wait_for(LoginPageLocators.LOGIN_PAGE_LINK).click()
        assert LoginPageLocators.LOGIN_PAGE_URL in self.driver.current_url

    def enter_email(self, email):
        self.wait_for(LoginPageLocators.EMAIL_INPUT).clear()
        self.wait_for(LoginPageLocators.EMAIL_INPUT).send_keys(email)

    def enter_password(self, password):
        self.wait_for(LoginPageLocators.PASSWORD_INPUT).clear()
        self.wait_for(LoginPageLocators.PASSWORD_INPUT).send_keys(password)
            
    def submit(self):
        self.find_element(LoginPageLocators.SUBMIT_BUTTON).click()

    def check_error_message(self):
        error_message = self.wait_for(LoginPageLocators.ERROR_MESSAGE_LOCATOR)
        assert error_message.text == LoginPageLocators.ERROR_MESSAGE

Example of Login Page locators

  • Locators for all pages are included in the same file under specific class
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
from selenium.webdriver.common.by import By

"""
    Classes containing locators for specific pages
"""

class LoginPageLocators:

    LOGIN_PAGE_LINK = (By.LINK_TEXT, 'Signup / Login')
    LOGIN_PAGE_URL = '/login'

    EMAIL_INPUT = (By.XPATH, '//input[@data-qa="login-email"]')
    PASSWORD_INPUT = (By.XPATH, '//input[@data-qa="login-password"]')
    
    SUBMIT_BUTTON = (By.XPATH, '//button[@data-qa="login-button"]')

    ERROR_MESSAGE_LOCATOR = (By.XPATH, '//p[@style="color: red;"]')
    ERROR_MESSAGE = 'Your email or password is incorrect!'

Using POM really helps me separate various concerns and keeps the code DRY. Implementing the POM allows for better collaboration between developers and testers and ultimately leads to a more robust test automation framework