Many automation engineers find the pace of their work grueling. That's due in no small part to the cost of maintaining their automated tests. Years ago, a great advancement, the page object, an object repository in Selenium WebDriver, made maintenance much simpler.
But now, with software getting more complex, even page objects can cause their own flavor of maintenance cost. What will be the next paradigm in organizing UI testing? Pattern objects.
Here's why pattern objects are the next generation of page objects for test automation.
What is a page object?
A page object is a piece of code (a class) that holds all the information you need about a page in a web-based application. Its job is to represent everything you would need to interact with on a web page in order to quickly construct a more readable test. Page objects contain a mixture of locators for elements on the screen (such as text fields, buttons, dropdowns, or tables) and custom methods to interact with those elements.
A page object might look something like this:
class LoginPage {
string Username = “//input[@id='user']”;
string Password = “//input[@id='passwd']”;
string Login = “//input[@id='submit']”;
def setUsername(s) {
browser.findElement(Username).set(s);
}
def setPassword(s) {
browser.findElement(Password).set(s);
}
}
... followed by test code that uses the page object, which might look something like this:
LoginPage page = new LoginPage();
page.setUsername(“mfritzius”);
page.setPassword(“coolpassword”);
page.Login.click();
So page objects help with making test code more readable and intuitive. But they can actually start to cause problems as an application becomes more complex.
Where a page object falls short
Tribal knowledge. Page objects are great if you're the sole contributor to an automation project and you know your own coding style. But once multiple people—each with his or her own coding style—get added to the team, page objects can become unwieldy, because now:
- New additions to the team have to remember a bunch of standards, such as how page objects—and the methods and locators inside them—are named.
- They might have to spend a lot of time debugging test code.
- They have to ask the creator of the page objects a bunch of questions before any test code can be written.
After a while, a code base that uses page objects raises some common questions:
- Which page object are you supposed to use? There could be multiple ones that look similar.
- Which variable should be used when accessing a certain element? Maybe you've seen the variable tblCustomerRecords used in newer page objects but customerRecordsTable in the older code.
- If you're getting ready to make a new page bject, is there one available already for your purposes? How would you know?
Now, multiply that complexity by 10 or 100, and it's clear why this may not be the best method to drive a multi-tester automation effort. Each question, hesitation, or confusion consumes time that now can't be spent creating valuable tests.
Use modern UI frameworks to your advantage
Many modern front-end test automation frameworks—such as Angular, React, Bootstrap, and Kendo—let you code quickly and do so in a predictable way. The HTML, CSS, and JavaScript they generate follow particular rules and structures laid out in their frameworks, and I realized that testers can take advantage of this.
Instead of representing each page, we can represent each element type—elements that can all be located the same way regardless of where they are in the application because they were generated by a tool that followed a pattern. This new technique is what I call a pattern object.
What is a pattern object?
A pattern object is like a page object, but it focuses on patterns that can be found throughout an application. Instead of holding information about a specific page, a pattern object holds information about a specific element type—such as a button, a text field, or a table. Across the whole application, elements of a certain type can be located in the same way.
For example, let's say each button on your site has a text label indicating what it's used for. If you look at the web page code, you may notice that each button is of type <div>, has a class attribute containing the string “btn” somewhere, and has a value attribute equal to the text on the button. The HTML would look like this:
<div class=”enabled btn visible” value=”Click This Button” />
Now instead of creating a locator for each button on each page, you can create a pattern object that looks like this:
class PatternObject {
Then to use it:
def button = “//div[contains(@class, 'btn') and @value='xxx']”;
def clickButton(text)
browser.findElement(button.replace(“xxx”, text)).click();
}
}
PatternObject object = new PatternObject();
object.clickButton(“Click This Button”);
Because all buttons are the same, except for the value attribute, that becomes the variable part of the locator, and gets replaced on demand. The “xxx” in the pattern object is a special token that gets swapped out for the text on the button itself, so the correct one can be clicked. This can also be done for text fields, select lists, radio buttons, tables, sliders—everything. It just depends on what the pattern is for each of those element types in your specific application.
How a pattern object helps
For starters, it helps with code discipline. Many times, when applications are hard to test, it's because there's a lot of custom, patched-up code causing confusion. But as developers let their frameworks produce more predictable code—the code becomes more testable.
It also helps with maintenance. Hundreds of page objects can be replaced with a handful of pattern objects—or even just one. Having fewer moving parts decreases the time it takes to maintain, which allows more time to be devoted to writing tests.
Finally, it helps with test construction. If you're clever with your patterns, you can create generic locators that use information that's visible at the UI layer itself. As in the button example earlier, people can look at a page and identify the exact button they'd need to click, because:
- It looks like a button, and
- It's identifiable by human-readable text.
In that example, the only two pieces of data you need are readily available to your eyeballs. Translating that to code becomes trivial.
How to build intelligence in
The readability aspects of pattern objects can be extended even further. Case in point: What if an application has more than one way to locate a button? What if both of these patterns show up in two different places in the application?
<div class=”enabled btn visible” value=”Click This Button” />
<button value=”button01”>”Another Button”</button>
The simplest way to locate them is to use an OR statement to merge the two locators together, like this:
(//div[contains(@class, 'btn') and @value='xxx']||//button[text()='xxx'])
But a more readable way would be to store each locator in an array and loop through them until one of the locators returns exactly one hit:
buttons = [
“//div[contains(@class, 'btn') and @value='xxx']”,
“//button[text()='xxx']”
]
You can also include code that throws errors if more than one of the locators returns a hit. That code should include detailed help messages to aid in debugging.
The next evolution in test automation
Software products almost always increase their complexity over time. Testers need to think creatively if they want to keep that complexity under control. If you're ready for the next evolution in test automation, use pattern objects.
Tell me what you think about the idea in the comments section.
Keep learning
Take a deep dive into the state of quality with TechBeacon's Guide. Plus: Download the free World Quality Report 2022-23.
Put performance engineering into practice with these top 10 performance engineering techniques that work.
Find to tools you need with TechBeacon's Buyer's Guide for Selecting Software Test Automation Tools.
Discover best practices for reducing software defects with TechBeacon's Guide.
- Take your testing career to the next level. TechBeacon's Careers Topic Center provides expert advice to prepare you for your next move.