Revolution or Evolution of Page Object Model?

Artem Sokovets
12 min readNov 7, 2019

--

By Artem Sokovets

What do you know about the Page Object Pattern?

First, it was introduced at Selenium WebDriver in 2009. Different wrappers and design patterns such as Chain of Invocations, Page Element, Loadable Component, ScreenPlay, etc. appeared then. During all my experience I have seen that many UI test projects which use a different kind of frameworks based on Page Object Model (POM). With POM you should know that the web page represents as the class, and the various web elements on the page are defined as variables of the class.

I think it is time to look at the Page Object Pattern and improve it. What do you think what would be if we use interfaces instead of classes and interface methods vice class variables?

If you have an interest in this topic let’s dive deeper into the problems of Page Object Pattern (the article uses java code examples).

Nowadays problems

You may agree with my opinion or not but as for me, I have found a bit uncomfortable things during using POM. Here they are:

Duplication of elements

public class MainPage {
@FindBy(xpath = ".//div[@class = 'header']")
private Header header;
}

public class AnyOtherPage {
@FindBy(xpath = ".//div[@class = 'header']")
private Header header;
}

When I need to interact with different components in tests, such as header, footer or some another block, I must copy-paste this block to each page object class.

If you are not familiar with this approach, it is called Page Element wrapper (aka HTML Wrapper).

No parameterization for elements

public class EditUserPage {  
@FindBy(xpath = "//div[text()='Sing In']")
private Button activity;

@FindBy(xpath = "//div[text()='Sign Up']")
private Button reason;
}

There are many web elements that contain similar locators, but the text value of these locators is dissimilar. For example, you have two buttons on the page, but text value is a little bit different, in one case it could be “Sign In” and in another “Sign Up”.

Boilerplate code

public class UserPage {     
@FindBy(xpath = "//div[text()='Phone']/input")
private TextInput innerPhone;

@FindBy(xpath = "//div[text()='Email']/input")
private TextInput email;

@FindBy(xpath = "//button[text()='Save']")
private Button save;

@FindBy(xpath = "//button[text()='Users']")
private Button usersList;
}

When you have many identical web elements with getter methods in the class, you get boilerplate code.

Huge step class

public class MainSteps {
public void hasText(HtmlElement e, Matcher m)
public void hasValue(HtmlElement e, Matcher m)
public void linkContains(HtmlElement e, String s)
public void hasSize(List<HtmlElement> e, Matcher m)
public void hasItem(List<HtmlElement> e, Matcher m)
//Other methods
}

After some time, the steps class is becoming bigger and bigger. In this class, you usually add useful methods for web elements. Such kind of class is hard to maintain and you need to have an eagle eye to avoid duplicates of methods.

Your Page Object Assistant

I am pleased to introduce the next version of Selenium Framework (Html Elements) which has the goal to resolve problems that were described before, reduce boilerplate code, to manage the list of web element more comfortably, retry your automated tests, and last but not least more flexible configuration using extension model.

Atlas — Next Generation Java framework for UI automated tests that use a new approach in the declaration of Page Object through an interface.

Furthermore, you can use multiple inheritances, default methods, a clear tree structure of web elements, embedded waiting and assertions based on Hamcrest Matchers.

The main feature of the next version is the using of interfaces instead of standard classes.

Let’s see first Page Object based on the interface:

public interface MainPage extends WebPage, WithHeader {
@FindBy("//a[contains(text(), 'Or start a free)]")
AtlasWebElement trial();
}

In the code above, I described the main page of GitHub with one web element.

Architecture Framework

Atlas is composed of several different modules:

  • atlas-core
  • atlas-webdriver
  • atlas-appium

In the atlas-core, you can find the main functionality of how to handle page objects pages through Proxy Pattern (dynamic invoke). The idea to represent Page Object as the interface was taken from Retrofit.

The other two modules atlas-webdriver and atlas-appium are used to develop UI WEB and UI Mobile automated tests. The main entrance for UI WEB pages is WebPage interface, and for UI Mobile is Screen interface. Conceptually both modules consist of extensions (package *.extension).

Atlas Elements

There are two specialized classes (AtlasWebElement and AtlasMobileElement) which are used for processing web elements (the same as WebElement from Selenium API).

Let’s see the class diagram:

In addition, AtlasWebElement and AtlasMobileElement have should(), waitUntil() and other powerful methods besides standard WebElement methods (click(), sendKeys() and etc). Atlas suggests features to do cross-platform elements by extending both classes above.

Atlas Features

The main features on the image below.

Interfaces instead of classes

In the new approach, you should use interfaces when creating the page object.

public interface ProjectPage extends WebPage, WithHeader {
@FindBy("//a[contains(.,'contributors')]")
AtlasWebElement contributors();
}

It is a simple example, that describes one web element on the Github project page.

Parameterization of web elements

Let’s say that we have the form with several text inputs.

In the traditional way, I need to create 11 web elements using the page object model with @FindBy annotation from the Selenium API. Furthermore, you also need to declare getter methods in special cases.

With Atlas, you need one web element and that’s all. Yes, only one it’s not a joke.

public interface MainPage extends WebPage {
@FindBy("//div[text()='{{ text }}']/input")
AtlasWebElement input(@Param("text") String text);
}

Text in single quotes will change to your value when you set the method parameter. The automated test will look like this:

@Test
public void simpleTest() {
onMainPage().input("First Name").sendKeys("*");
onMainPage().input("Postcode").sendKeys("*");
onMainPage().input("Email").sendKeys("*");
}

First, go to page context, next call parametrized method with necessary arguments and after that make actions that are needed.

Multiple Inheritances

As I said before, if you copy-paste block (for example header or footer) to different page objects you will have a duplicate of code. But you can avoid boilerplate code.

I have a GitHub header.

Let’s describe web elements that are contained in this header (most of them are omitted).

public interface Header extends AtlasWebElement {
@FindBy(".//input[contains(@class,'header-search-input')]")
AtlasWebElement searchInput();
}

Next, create a layer that can be used in any page objects.

public interface WithHeader {
@FindBy("//header[contains(@class,'Header')]")
Header header();
}

Extend the main page with a layer.

public interface MainPage extends WebPage, WithHeader {
@FindBy("//a[contains(text(), 'Or start a)]")
AtlasWebElement trial();
}

In addition, you could declare many layers as you want and set them up to any page objects. This approach offers us to set one layer (one block) without copy-pasting classes’ variables to each page object.

In the example below, I set different layers to the main page such as header, footer, sidebar.

public interface MainPage extends WithHeader, WithFooter, WithSidebar {}

Going deeper! GitHub header contains 4 buttons, 3 dropdown lists, and one text input.

Let’s create buttons’ wrapper and using a parameterization of web elements to describe 4 buttons by one element.

public interface Button extends AtlasWebElement {
@FindBy(".//a[contains(., '{{ value }}')]")
AtlasWebElement selectButton(@Param("value") String value);
}

Add a wrapper to the header layer. By that way, I inflate the functionality of the header.

public interface Header extends WithButton {
...
}

As before, you could add the above wrapper to any layers and get additional functionality.

How to use multiple inheritances?

@Test
public void simpleTest() {
onMainPage().open("https://github.com");
onMainPage().header().button("Priсing").click();
}

In the second line of the example, I refer to the GitHub main page, next go to the header layer, then set value for parameterized element and at last click on the web element.

In your system under test (web site) there could be a lot of web elements, which are repeated from one-page object to another. You can choose what approach is the best in your case, but don’t repeat yourself. Remember about web elements tree and one to many relationships.

Default methods

As you are familiar with Java, you know that default methods were introduced in JDK 8. These methods are used to declare standard behavior in interfaces.

Let’s say you have many automated scenarios that come through one checkbox which could be turned off/turned on. In various situations, you need to turn on the checkbox, when it is turned off.

if(onMainPage().rentFilter().checkbox("Brick").getAttribute("class").contains("disabled")) {
onMainPage().rentFilter().checkbox("Brick").click();
}

In order not to store all this code in the step class, it is possible to place it next to the wrapper as a default method.

public interface Checkbox extends AtlasWebElement {    @FindBy(...)
AtlasWebElement checkBox((@Param("value") String value);

default void selectCheckbox(String value) {
if(checkBox(value).getAttribute("class").contains("disabled")) {
checkBox(value).click();
}
}
}

In an automated test, it should be like this:

onMainPage().rentfilter().selectCheckbox("Brick");

Another example where you could use the default method is to combine clear and sendKeys methods in one action.

onMainPage().header().input("GitHub").clear();
onMainPage().header().input("GitHub").sendKeys("*");

Define a method that clears the text input and returns the web element itself:

public interface Input extends AtlasWebElement {   @FindBy("//xpath")
AtlasWebElement input(@Param("value") String value);
default AtlasWebElement withClearInput(String value) {
input(value).clear();
return input(value);
}
}

You could add any logic to default methods as much as necessary.

onMainPage().header().withClearInput("GitHub").sendKeys("Atlas");

In this way, you can program any behavior in the default methods for the wrapper element.

Embedded Retry

One of the main Atlas features is embedded retry. You shouldn’t care about exceptions like as ElementNotFoundException, StaleElementRederenceException, OtherElementReceiveTheClick.

Also, forget about explicit or implicit waits.

onSite().onSearchPage("Test").repositories().waitUntil(hasSize(10));

if one of these methods is broken all the methods’ chain will start again.

You can set custom retry configuration (by default timeout = 5 seconds and polling = 1 second):

Atlas atlas = new Atlas(new WebDriverConfiguration(driver))
.context(new RetryerContext(new DefaultRetryer(3000L, 1000L, Collections.singletonList(Throwable.class))));

Moreover, you can set additional annotation (@Retry) to a special web element which takes more load time. In the example below, I set the timeout to 20 seconds with 2 seconds polling.

@Retry(timeout = 20_000L, polling = 2000L)
@IOSFindBy(xpath = "//XCUIElementTypeSearchField[@name='Search Wikipedia']")
@AndroidFindBy(xpath = "//*[contains(@text, 'Search Wikipedia')]")
AtlasMobileElement searchWikipedia();

Easy way to process the list of web element

Atlas suggests an easy way to work with lists of web elements. What does it mean? Suppose you have the text input. When you set the value to this input drop-down list with web elements will appear.

In this way, the framework has the Elements Collection entity.

public interface ContributorsPage extends WebPage, WithHeader {
@FindBy(".//ol[contains(@class, 'contrib-data')]//li[contains(@class, 'contrib-person')]")
ElementsCollection<RepositoryCard> hovercards();
}

I declared a list of repository cards and set the XPath locator. RepositoryCard interface contains one element:

public interface RepositoryCard extends AtlasWebElement<RepositoryCard> {
@FindBy(".//h3")
AtlasWebElement title();
}

Next, you can manipulate the list in the usual way. For example, check the size of the collection with Hamcrest Matcher.

onSite().onContributorsPage().hovercards().waitUntil(hasSize(4));

If you want to transform values to other types use extract() method (as java stream map() method).

Smart Assertions

AtlasWebElement, AtlasMobileElement, and ElementsCollection have assertion methods as I considered before. There are two overload methods:

╔═════════════════╦════════════════════════════════════════════════╗
║ Method ║ Overview ║
╠═════════════════╬════════════════════════════════════════════════╣
║ T should(…); ║ Making an assertion for web element. Set ║
║ ║ Matcher Object from Hamcrest library element. ║
║ ║ Throws AssertionError.
╠═════════════════╬════════════════════════════════════════════════╣
║ T waitUntil(…); ║ Making an assertion for web element. Set ║
║ ║ Matcher Object from Hamcrest library element. ║
║ ║ Throws RuntimeException.
╚═════════════════╩════════════════════════════════════════════════╝

The main idea is having a similar method with one little difference in the throwable object. Now you could save time when you check the report of running automated tests. Most of the functional checks run at the end of automated tests — this is interesting for functional QA engineer (product defects), but intermediate checks are interesting for automated QA engineer (problems with automated tests or test environment).

You immediately could make a decision about what is wrong with the Allure report.

Extension model

Atlas provides user’s extensions that can be used to tap into the events of the framework. The idea is taken from Junit 5. Both atlas modules (atlas-webdriver and atlas-appium) use the extension model. If you have an interest you should check the package with the name “extension” in source code.

Consider this with a specific example.

onMainPage().header().button("en").click();

You want to override click method on the whole test project (change selenium click to js-click). It is not a good idea in the real test project.

Create a class that implements the MethodExtension interface.

public class JSClickExt implements MethodExtension {

@Override
public Object invoke(Object proxy, MethodInfo methodInfo, Configuration config) {
final WebDriver driver = config
.getContext(WebDriverContext.class)
.orElseThrow(() -> new AtlasException("Context doesn't exist")).getValue();

final JavascriptExecutor js = (JavascriptExecutor) driver;
js.executeScript("arguments[0].click();", proxy);

return proxy;
}

@Override
public boolean test(Method method) {
return method.getName().equals("click");
}
}

In the above code, I implemented two methods. First test(…) checks that click method is used, second invoke(…) runs javascript click.

After that, you need to register extension when initializing atlas class:

atlas = new Atlas(new  WebDriverConfiguration(driver, "https://github.com")).extension(new JSClickExt());

If you want to implement your own logic of finding elements or something bigger, you should use the extension model.

Site Entity

The framework allows you to store all your Page Objects in one place and work only through the Site entity.

public interface GitHubSite extends WebSite {

@Page
MainPage onMainPage();

@Page(url = "search")
SearchPage onSearchPage(@Query("q") String value);

@Page(url = "{profile}/{project}/tree/master/")
ProjectPage onProjectPage(@Path("profile") String profile, @Path("project") String project);

@Page
ContributorsPage onContributorsPage();
}

In addition, it is possible for Page Objects to specify URL, query parameters, and path segments.

Going with the above site entity idea, consider the example:

onSite().onProjectPage("qameta", "atlas").contributors().click();

How do you think, what does this code line mean? Okay, let’s continue. I won’t blow your brain.

Both path segments will transform into URL: https://github.com/qameta/atlas/tree/master/.

The base URL is set when you initialize atlas class:

atlas = new Atlas(new WebDriverConfiguration(driver, "https://github.com"));

The main goal of the site entity is to go to the needed page immediately without monkey clicking.

@Test
public void usePathWebSiteTest() {
onSite().onProjectPage("qameta","atlas").contributors().click();
onSite().onContributorsPage().hovercards().waitUntil(hasSize(4));
}

AtlasMobileElement

Working with a mobile element (AtlasMobileElement) is realized in the same way as with the web element (AtlasWebElement).

In addition, AtlasMobileElement has three methods: scrollUp(), scrollDown() and longPress();

In mobile automation, you need to implement a Screen interface. With AtlasMobileElement you can use locator for two platforms (IOS, Android), make parameterization and other awesome things that I considered above.

public interface MainScreen extends Screen {

@Retry(timeout = 20_000L, polling = 2000L)
@IOSFindBy(xpath = "//XCUIElementTypeSearchField[@name='Search Wikipedia']")
@AndroidFindBy(xpath = "//*[contains(@text, 'Search Wikipedia')]")
AtlasMobileElement searchWikipedia();

@IOSFindBy(id = "{{ value }}")
AtlasMobileElement button(@Param("value") String value);
}

Example of the mobile test using JUnit 4:

@Test
public void simpleExample() {
onMainScreen().searchWikipedia().click();
onSearchScreen().search().sendKeys("Atlas");
onSearchScreen().item("Atlas LV-3B").swipeDownOn().click();
onArticleScreen().articleTitle().should(allOf(displayed(), text("Atlas LV-3B")));
}

The code executes the next steps: opening the main screen of the Wikipedia mobile application, moving to search input, typing Atlas, scrolling to the element with text Atlas LV-3B and going into element entity. At last, making the assertion (should method with a set of Matcher objects) that the title’s header entity contains valid text.

Listener

Atlas provides a Listener interface that helps to catch method events: before, pass, fail, after.

The framework can build a report for any report systems as you want using the listener. Check the link if you need an Allure report example (it’s an example).

Creating the listener instance:

atlas = new Atlas(new WebDriverConfiguration(driver)).listener(new AllureListener());

Setup

For WEB automation you can use the atlas-webdriver module with the last version from a central repository. The available version is 1.7.0.

Maven:

<dependency>
<groupId>io.qameta.atlas</groupId>
<artifactId>atlas-webdriver</artifactId>
<version>${atlas.version}</version>
</dependency>

Gradle:

dependencies { сompile 'io.qameta.atlas:atlas-webdriver:1.+' }

For mobile automation testing — atlas-appium module. The version is the same.

Maven:

<dependency>
<groupId>io.qameta.atlas</groupId>
<artifactId>atlas-appium</artifactId>
<version>${atlas.version}</version>
</dependency>

Gradle:

dependencies { сompile 'io.qameta.atlas:atlas-appium:1.+' }

Getting Started

When you add a dependency to your test project first step you need is to initialize Atlas class.

@Before
public void startDriver() {
driver = new ChromeDriver();
atlas = new Atlas(new WebDriverConfiguration(driver));
}

Atlas constructor gets one parameter WebDriverConfiguration with the driver. For mobile automation, you should use AppiumDriverConfiguration. Both configuration classes contain default extensions.

Next, define the utility method that creates PageObjects (Screens):

private <T extends WebPage> T onPage(Class<T> page) {
return atlas.create(driver, page);
}

Consider the simple example as before:

@Test
public void simpleTest() {
onPage(MainPage.class).open("https://github.com");
onPage(MainPage.class).header().searchInput().sendKeys("Atlas");
onPage(MainPage.class).header().searchInput().submit();
}

Open the main page of GitHub, go to header block, then type Atlas in the search input. After that, click the submit button.

Takeaways

In the end, I want to note, that Atlas is a flexible instrument with cool features. You can make custom configuration as you or your team wants. Try it — develop cross-platform automated tests and reduce your boilerplate code. For more examples check source code. Don’t forget to set Github ⭐️ , if you liked the Atlas.

Furthermore, watch the video from the SQADAY_EU conference and follow me on twitter.

--

--