top of page

Revolutionizing Pytest Testing with Page Object Model

Zaktualizowano: 2 sie 2023

Welcome to the next chapter of our software testing journey! In this post, we will revamp our existing code to follow a more Page Object Model (POM)-ish approach.


By doing so, we aim to make our codebase more organized, maintainable, and efficient, all while adhering to the principles of Keep It Simple (KISS), Don't Repeat Yourself (DRY), and You Aren't Gonna Need It (YAGNI).


The Page Object Model is a popular design pattern used in test automation to create an abstraction of web pages, allowing us to interact with the application's elements through methods defined in those abstractions. This approach not only improves code readability but also promotes code reusability and maintainability.


Our current code includes separate test class, a still empty configuration file (conftest.py), and page objects for different sections of the website. However, we want to make it even better by introducing the POM concept.


Let's start by taking an iterative approach to building our POM-based framework. Our primary goals are to keep the code simple and readable (KISS), leverage inheritance for code reuse (DRY), and only implement what we currently need and nothing above(YAGNI).

Here are the key steps we'll take to refactor our code:


Step 1: Setting Up the Foundation - conftest.py


To begin our journey, we need to set up the foundation for our test automation framework. The conftest.py file will act as a central configuration file where we define fixtures, including our essential "init_browser" fixture. This fixture returns the browser driver using Selenium and ChromeDriverManager, enabling seamless integration with Chrome.


# conftest.py
import pytest
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager

@pytest.fixture(autouse=True, scope="class")
def init_browser(request):
    """Returns the browser driver for each test class."""
    options = webdriver.ChromeOptions()
    options = set_browser_options(options)
    chrome_driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()), options=options)
    request.cls.driver = chrome_driver
    # return chrome_driver
    yield chrome_driver
    chrome_driver.close()

def set_browser_options(options):
    """Set Chrome browser options."""
    options.add_argument("window-size=2560x1440")
    options.add_argument('--headless')
    options.add_argument("enable-automation")
    options.add_argument("start-maximized")
    options.add_argument("--disable-gpu")
    options.add_argument("--disable-dev-shm-usage")
    options.add_argument("--ignore-certificate-errors")
    options.add_argument("--ignore-ssl-errors=yes")
    options.add_experimental_option("excludeSwitches", ["enable-logging"])
    return options

Step 2: Creating a BasePage - base_page.py


Next, let's create a BasePage class that serves as the backbone of our Page Object Model. This class will contain common methods and utilities shared by all the pages in our application. With fluent waits and reusable methods, we ensure that our tests are resilient and efficient.


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

class BasePage:
    def __init__(self, driver):
        """Initialize the BasePage with the browser driver."""
        self.driver = driver

    def fluent_click(self, locator):
        """Perform fluent click using WebDriverWait."""
        element = WebDriverWait(self.driver, 10).until(
            EC.element_to_be_clickable(locator)
        )
        element.click()


Step 3: Building Page Classes - tools_qa_homepage.py and elements_page.py


With the BasePage in place, it's time to construct our page classes. These page classes, such as ToolsQAHomePage and ElementsPage, will inherit from the BasePage, encapsulating specific functionality unique to each page. The fluent_click method and is_elements_page_loaded validation are excellent examples of the power of POM.



# tools_qa_homepage.py
from selenium.webdriver.common.by import By
from pages.base_page import BasePage
from pages.elements_page import ElementsPage

class ToolsQAHomePage(BasePage):

    def navigate_to_home_page(self):
        """Navigate to the ToolsQA home page."""
        self.driver.get('https://demoqa.com/')

    def navigate_to_elements_page(self):
        """Navigate to the Elements page."""
        elements_link = self.driver.find_element(By.XPATH, '//h5[contains(text(), "Elements")]')
        self.fluent_click(elements_link)  # Utilize the fluent_click method from BasePage
        return ElementsPage(self.driver)

and

# elements_page.py
from pages.base_page import BasePage
from selenium.webdriver.common.by import By

class ElementsPage(BasePage):

    def is_elements_page_loaded(self):
        """Check if the Elements page is loaded."""
        return "Elements" in self.driver.find_element(By.CSS_SELECTOR, '.main-header').text


Step 4: Creating Test Classes - base_web_test.py and test_navigate_elements_page.py


As we move on, we set the stage for our test classes. The BaseWebTest class, marked with the "init_browser" fixture, serves as an interface for incoming test classes that will test the web. With this setup, we achieve code reuse, making our test classes concise and focused on testing specific functionality.



# base_web_test.py
import pytest

@pytest.mark.usefixtures("init_browser")
class BaseWebTest:
    """Base test class that sets up the browser fixture."""
    pass


In the test_navigate_elements_page.py, we create the TestNavigationPage class, inheriting from BaseWebTest. We can now easily write and execute tests by leveraging the power of the "init_browser" fixture, which automates browser setup and teardown.


# test_navigate_elements_page.py
from pages.tools_qa_homepage import ToolsQAHomePage
from tests.base_web_test import BaseWebTest

class TestNavigationPage(BaseWebTest):
    """Test class for navigating to the Elements page."""
    def test_navigate_to_elements_page(self):
        home_page = ToolsQAHomePage(self.driver)  # Create an instance of the ToolsQAHomePage
        home_page.navigate_to_home_page()  # Navigate to the home page
        elements_page = home_page.navigate_to_elements_page()  # Click on the "Elements" link to navigate to the
        # Elements page
        assert elements_page.is_elements_page_loaded()  # Verify that the Elements page is loaded


To test our code you can use simple "runner"


import pytest

if __name__ == '__main__':
    # Set up pytest arguments
    args = [
        '-v',     # Verbose output
        '-s',     # Print output to console
    #    '--html=report.html',  # HTML test report
    #    '--junitxml=report.xml',  # JUnit XML test report
        'tests/',  # Path to test directory
    ]

    # Call pytest with arguments
    pytest.main(args)

My code is structured like this (please ignore oldtest_load_page_then_verify_title, this is nothing else but code of our former test witch changed name so pytest will ignore it):

Project file structure after refactor
Project file structure after refactor

Debug

I have added this section becasue you might find a peculiar problem while running your tests.

I assume at this point you have at least refactored the code. Now you will be tempted to use the tiny green triangle to start your tests which will end in failure. Terminal output will say that fixture was not found.

So let us check.

Type in terminal :


pytest - - fixtures


What you should see is list of fixtures with init_browser being annotated with "loaded from conftest" - so how come it does not work?

Well problem is when you run from terminal whole project is being loaded with conftest first then init. py then "test_something_something.py" which means fixture is being resolved first.

If you run using the IDE conftest apparently is resolved later. Hence the error. Now to avoid it you can add a temporary import :


from conftest import init_browser


Why temporary? Becasue it will be greyed out and prone to import optimisation.


So best idea is to either stick to the runner I shared or use terminal when using pytest to avoid unpleasant surprises.



Congratulations! You've successfully refactored your test code to introduce Page Object Model (POM)-like organization and implemented tests following the principles of KISS, YAGNI, and DRY. By centralizing fixtures, creating a base page, and building page classes, you've taken a significant step towards cleaner, more maintainable, and scalable test automation.



Happy testing!

Comments


Subscribe to QABites newsletter

Thanks for submitting!

  • Twitter
  • Facebook
  • Linkedin

© 2023 by QaBites. Powered and secured by Wix

bottom of page