Selenium is one of the most popular tools when it comes to testing automation. You can use it in a variety of ways, some of which do not result in clean, concise code. Fortunately, there are design patterns you can leverage to make your Selenium tests more organized and structured. This post covers one of them: the Page Object Model pattern, or simply POM. By the end of the post, you’ll understand what POM is, why you should care about it, and how to use it. Here’s what we’ll cover:
- What is Page Object Model in Selenium?
- What are the benefits of adopting the Page Object Model pattern?
- A Page Object Model example
- What’s the difference between POM and Page Factory?
Before we start, let’s talk about requirements. Throughout this post, we assume you have at least some experience with Selenium, and we assume you already have the ChromeDriver executable downloaded on your machine and put it on the system’s PATH. Since we’ll be using Python for the examples, having the language installed (I’m using 3.11.0) and knowing how to install packages via pip will make things easier.
What is Page Object Model in Selenium?
If you Google “Selenium page object model,” you’ll find plenty of articles describing it as “a design pattern used to create an abstraction layer” or something to that effect. Let’s try simpler language. The page object model (POM) practice is a way to organize your code when writing tests that leverage Selenium. Using this pattern, you represent each page from a web application as an object, creating methods that expose the behaviors of the page. Your test code then calls the methods from the page object models during the three phases (arrange-act-assert) of a typical test. Now that you know the definition of page object models, let’s learn the reasons for adopting them.
What are the benefits of adopting the Page Object Model Pattern?
The benefits of adopting the POM pattern are all about code cleanliness and test resilience. When you use a POM to abstract the behaviors of a given page, code that accesses and exercises elements from that page is concentrated in a single place. That way, you eliminate a lot of code duplication that would happen otherwise. As a consequence of less duplication, test maintenance also becomes easier. How is that so? With a POM, code that uses locators to find the given page’s elements resides in the same place. If the locator changes—for instance, a CSS class is renamed—you’d only have to make changes in a single place: the page model. Without POM, you’d have to perform many changes through the test codebase since code accessing a page’s locators could be anywhere.
Expand Your Test Coverage
A Page Object Model example
First, let’s start with a test that doesn’t use POM.
Non-POM test
For our sample test, we’ll use the demo website demoblaze.com. It’s a fake e-commerce site you can use for testing purposes.
from selenium import webdriver from selenium.webdriver.common.by import By def test_title(): driver = webdriver.Chrome() driver.get("https://www.demoblaze.com/cart.html") heading_div = driver.find_element(By.CLASS_NAME, "col-lg-8") first_child = heading_div.find_element(By.XPATH, "./*[1]") expected = "Products" actual = first_child.text assert actual == expected, f"Expected '{expected}', got {actual}" driver.quit() if __name__ == "__main__": test_title()
The piece of code above is a simple Python test using Selenium. It uses a ChromeDriver instance to navigate to the cart page on the demoblaze site. There, it retrieves the element identified by the CSS class “col-lg-8”. Then, it gets the first child of said element using XPATH and asserts that the inner text of this element is “Products”, as expected. This test above is super simple, and in a real setting, you probably wouldn’t use the POM pattern. But consider it a simple proxy for more complex tests. Imagine that the test above is only one among many that interact with the same page. In this scenario, if the col-lg-8 identifier changes, you’d have to change it in several places in the test code.
Leveraging POM
How would you go about implementing a page object model here? For starters, let’s create a class that abstracts access to the shopping cart page:
from selenium import webdriver from selenium.webdriver.common.by import By class CartPage: def __init__(self, driver): self.driver = driver self.heading_div = driver.find_element(By.CLASS_NAME, "col-lg-8") self.first_child = self.heading_div.find_element(By.XPATH, "./*[1]") def get_first_child_text(self): return self.first_child.text
The class is quite simple: it starts by importing the web driver dependencies. Then, on its constructor, it uses the driver object to locate the desired elements, just like we did previously. And then, we have the most important part of the class: the get_first_child_text() method, which returns the text from the target element. We can now rewrite the test so it makes use of the newly created class:
from selenium import webdriver from selenium.webdriver.common.by import By from cart import CartPage def test_title(): # arrange driver = webdriver.Chrome() driver.get("https://www.demoblaze.com/cart.html") cart = CartPage(driver) expected = "Products" # act actual = cart.get_first_child_text() # assert assert actual == expected, f"Expected '{expected}', got {actual}" driver.quit() if __name__ == "__main__": test_title()
As you can see, the test starts the same, by creating the web driver object. But then, instead of trying to locate the desired element, this job is delegated to an instance of the CartPage class that receives the driver as an argument. Again, to fully appreciate why this approach is superior, imagine that we had not one but many tests that exercised the shopping cart page. Also, imagine that the CartPage class exposes many more methods instead of a single one. In this scenario, this approach means all the code that locates elements is in a single place, drastically reducing code duplication and test maintenance effort.
What is the difference between POM and PageFactory?
Implementing the POM pattern isn’t hard if you’re already experienced with Selenium testing. But what if I told you there’s an even easier way? As it turns out, Selenium offers a built-in class to facilitate the usage of the page object model pattern, called PageFactory. I’ll start by installing the necessary Python package:
pip install selenium-page-factory
Then, let’s create a new test class:
from seleniumpagefactory import PageFactory class CartPage2(PageFactory): def __init__(self, driver): self.driver = driver locators = { 'heading': ('CSS', ".col-lg-8 > h2") } def get_header_text(self): return self.heading.get_text()
Notice how this class’s constructor is shorter than the previous one. We no longer use the constructor to locate the element. Instead, there’s now an attribute called locators, which is a dictionary containing an identifier and the locator for the given element. As you can see, we’re trying to locate an <h2> element inside something marked with the col-lg-8 CSS class. If we needed to locate more elements, we’d simply add more items to locators. Finally, we expose a method that, in turn, calls the get_text() method on heading, which is an object that points to the located element. get_text() is an extended WebElement method provided by the PageFactory class. Here’s the complete list of extended methods you can leverage:
- set_text
- clear_text
- double-click
- select_element_by_text
- select_element_by_value
- get_list_selected_item
- is_Enabled
- getAttribute
- visibility_of_element_located
- element_to_be_clickable
- context_click
- click_and_hold get_text
- click_button
- get_list_item_count
- select_element_by_index
- get_all_list_item
- highlight
- is_Checked
- hover
- invisibility_of_element_located
- text_to_be_present_in_element
- execute_script
- release
Wrapping up
The Page Object Model is a useful design pattern for testing code. It leads to a cleaner and more concise test code, less code duplication, and more robust and easy-to-maintain tests. As if implementing the pattern wasn’t easy enough, you can leverage the PageFactory class to make your life even easier.
No matter what approach you decide to use, remember to follow this rule: no assertion methods inside the page object classes! Those classes should be used only for abstracting access to the page elements. The assertions themselves should still be put in the test classes. That way, you achieve a nice separation between test logic code and code that retrieves and manipulates web elements.