Page Object Model is universally applicable in web test automation
Using Page Object Model wisely to enhance automated test maintenance
Last year, I was requested to do a Test Automation POC (proof of concept) in a large financial company. The project manager, under the influence of a principal software engineer, wanted to implement a set of acceptance tests in different frameworks/languages (Java, JavaScript and Ruby). The plan for POC was three months. I accomplished the core tasks within three days.
How? I started with RSpec (Ruby) first, then re-implemented the tests in Java and JavaScript quickly. One key practice is that I used Page Object Model (POM) the same way in all languages (including C# and Python, which were not included in this POC). In the article, I will share my approach.
Table of Contents:
· What is Page Object Model?
∘ Demo
· Test Scripts: similar among all versions
· Page Classes: similar too
· How did I create with multiple versions of test scripts quickly?
· If one works well, why bother another language?
· Q & A
What is Page Object Model?
Page Object Model, also known as Page Object Pattern, is a well-known test design pattern in test automation, in fact, it is listed as one of the best practices on the Selenium website. The POM concept existed at least 30 years ago. One test manager (I still remember from Canberra) told me that he was happy to see my tool TestWise with built-in support for POM, which he had used this pattern in the 1980s.
The purpose of POM is “enhancing test maintenance and reducing code duplication” (quoted from the Selenium website). I believe many experienced test automation engineers have heard of it. I covered POM in my book: Practical Web Test Automation, and also in this article: Maintainable Automated Test Design. Read them or other resources on the Internet if you are not clear with POM.
I will continue to show how I used it in that POC.
Demo:
Firstly, I will show executions of a set of Selenium tests, which perform the same testing, but in different languages: Ruby, Python, and JavaScript.
Test Scripts: similar among all versions
For all three versions, the test scripts (the top tier), by using POM, look quite similar.
RSpec (Ruby) version:
before(:each) do
driver.get(site_url)
endit "[2] Return trip" do
flight_page = FlightPage.new(driver)
flight_page.select_trip_type("return")
flight_page.select_depart_from("Sydney")
flight_page.select_arrive_at("New York") flight_page.select_depart_day("02")
flight_page.select_depart_month("May 2021")
flight_page.select_return_day("04")
flight_page.select_return_month("June 2021")
flight_page.click_continue expect(page_text).to include("2021-05-02 Sydney to New York")
expect(page_text).to include("2021-06-04 New York to Sydney")
endit "[3] One-way trip" do
flight_page = FlightPage.new(driver)
flight_page.select_trip_type("oneway")
flight_page.select_depart_from("Sydney")
flight_page.select_arrive_at("New York") flight_page.select_depart_day("02")
flight_page.select_depart_month("November 2021")
flight_page.click_continue expect(page_text).to include("2021-11-02 Sydney to New York")
end
Pytest (Python) version:
def setUp(self):
self.driver.get(self.site_url())def test_select_return_flight(self):
flight_page = FlightPage(self.driver)
flight_page.select_trip_type("return")
flight_page.select_depart_from("Sydney")
flight_page.select_arrive_at("New York")
flight_page.select_depart_day("02")
flight_page.select_depart_month("May 2021")
flight_page.select_return_day("04")
flight_page.select_return_month("June 2021")
flight_page.click_continue()
self.assertIn("2021-05-02 Sydney to New York",
self.driver.find_element(By.TAG_NAME, "body").text)
self.assertIn("2021-06-04 New York to Sydney",
self.driver.find_element(By.TAG_NAME, "body").text)def test_select_oneway_flight(self):
flight_page = FlightPage(self.driver)
flight_page.select_trip_type("oneway")
flight_page.select_depart_from("Sydney")
flight_page.select_arrive_at("New York")
flight_page.select_depart_day("02")
flight_page.select_depart_month("November 2021")
flight_page.click_continue()
self.assertIn("2021-11-02 Sydney to New York",
self.driver.find_element(By.TAG_NAME, "body").text)
Mocha (JavaScript) version:
beforeEach(async function() {
this.timeout(timeOut);
await driver.get(helper.site_url());
});it ('[2] Return trip', async function() {
this.timeout(timeOut);
let flight_page = new FlightPage(driver);
await flight_page.selectTripType("return")
await flight_page.selectDepartFrom("Sydney")
await flight_page.selectArriveAt("New York")
await flight_page.selectDepartDay("02")
await flight_page.selectDepartMonth("May 2021")
await flight_page.selectReturnDay("04")
await flight_page.selectReturnMonth("June 2021")
await flight_page.clickContinue() await driver.findElement(By.tagName("body")).getText().then(function(the_page_text) {
assert(the_page_text.includes("2021-05-02 Sydney to New York"))
assert(the_page_text.includes("2021-06-04 New York to Sydney"))
});
});it ('[3] One-way trip', async function() {
this.timeout(timeOut);
let flight_page = new FlightPage(driver);
await flight_page.selectTripType("oneway")
await flight_page.selectDepartFrom("New York")
await flight_page.selectArriveAt("Sydney")
await flight_page.selectDepartDay("02")
await flight_page.selectDepartMonth("May 2021")
await flight_page.clickContinue() await driver.findElement(By.tagName("body")).getText().then(function(the_page_text) {
assert(the_page_text.includes("2021-05-02 New York to Sydney"))
});
});
Page Classes: similar too
The page class definitions are quite similar too.
Ruby version:
require File.join(File.dirname(__FILE__), "abstract_page.rb")class FlightPage < AbstractPage
def initialize(driver)
super(driver, "")
end def select_depart_from(from_port)
Selenium::WebDriver::Support::Select.new(driver.find_element(:name, "fromPort")).select_by(:text, from_port)
end def click_continue
driver.find_element(:xpath, "//input[@value='Continue']").click
end # other functions ...end
Python version:
from pages.abstract_page import AbstractPagefrom selenium import webdriver
from selenium.webdriver.support.ui import Select
from selenium.webdriver.common.by import Byclass
FlightPage(AbstractPage):
def select_depart_from(self, from_port):
Select(self.driver.find_element(By.NAME, "fromPort")).select_by_visible_text(from_port)
def click_continue(self):
self.driver.find_element(By.XPATH, "//input[@value='Continue']").click() # other functions
JavaScript version:
const AbstractPage = require('./abstract_page');var webdriver = require('selenium-webdriver'),
By = webdriver.By,
until = webdriver.until;class FlightPage extends AbstractPage { constructor(driver) {
super(driver);
} async selectDepartFrom(city) {
await this.driver.findElement(By.name("fromPort")).sendKeys(city);
}
async clickContinue() {
await this.driver.findElement(By.xpath("//input[@value='Continue']")).click();
} // other functions
}
Please note that I don’t use some kind of ‘page-object’ libraries, such as SitePrism, which often complicate things unnecessarily (or worse, wrongly), just a simple page class. You can find the full test scripts on Github, or the samples/demo
folder in TestWise.
Some readers might think, “The examples you showed are all Selenium WebDriver, how about other frameworks?”. The answer is that this approach works for all. I included examples in Playwright in the following article,
How did I create multiple versions of test scripts so quickly?
Keep reading with a 7-day free trial
Subscribe to The Agile Way to keep reading this post and get 7 days of free access to the full post archives.