Writing automated tests that are easy to maintain require skill, practice, discipline and good design. There are several design patterns that structure tests for re-use and maintainability. Page Objects is a popular pattern that comes built-in with Selenium.

Yet, as a tester who has been a developer before, I find Page Object Model (POM) counterproductive.

POM makes change hard to manage as it violates good design principles. This article talks about the violations with examples and shows a better solution.

For this, we will use the test cases written for the sample web application Active admin store.

Selenium’s recommendations

Here is a code snippet modeled on recommendations at SeleniumHQ.

Here, we use Selenium’s page factory. The UI elements are fields, annotated with locator details. But, when you look at usages, the method logIn() is used in many test cases. However the fields e.g. There is only one usage of the field login (line 30).

Also, elements on a webpage are usually linked to one action. e.g. Entering text in a Textbox or clicking a Button.

This pattern, prematurely designs UI elements for reuse violating the YAGNI principle.

“You aren’t gonna need it” (acronym: YAGNI) states that a programmer should not add functionality until deemed necessary.

Let’s fix this by removing all that upfront design by using locators from webDriver . Here’s the same example modified to do that.

Looks much better, but there’s more work.

Everything in its right place?

Page Objects get huge and difficult to maintain. One reason is because it has actions, locators and groups of unrelated functionality. For e.g. a product search on a page is unrelated to login or logout action. Thus breaking Single Responsibility principle (SRP).

The SRP states that every module or class should have responsibility over a single part of the functionality provided by the software, and that responsibility should be entirely encapsulated by the class.

Breaking Page objects to smaller page objects still violates SRP.

Consider tasks spanning pages/components. For e.g the login functionality.

The login task has 2 steps navigateToLoginPage in HomePage and loginCustomerWith in LoginPage.

LoginPage loginPage = new HomePage(driver).navigateToLoginPage();

loginPage.loginCustomerWith(customer, password);

As the actions belong to 2 different pages, this code will repeat in all test cases that use login . The responsibility is not entirely encapsulated. This is true even after extracting Header as a component.

Use the right abstractions!

Group by intent not page(s).

The modified LogIn code snippet below has only one reason to change. A change in the Log in functionality.

And this is why we built Gauge, to reuse intent.

We can reuse steps and concepts with parameters. These can span pages.

Responding to change

User flows != order of Pages.

Flow 1: Continue shopping with simple search. Created using websequencediagrams.com

Here is a user flow, where the user is taken to the Homepage on deciding to shop further.

When there’s a new flow for e.g shop after Advanced Search. The same action continue shopping ends with another page.

Flow 2: Continue shopping with advanced search. Created using websequencediagrams.com

A knee jerk reaction to this is using an interface e.g: SearchPage.

public class CartPage{

...

SearchPage continueShopping();

...

}

But then, what method(s) should SearchPage have?

public interface SearchPage{

SearchPage search(???);

}

And because they use different parameters use a SearchCriteria .

public interface SearchPage{

SearchPage search(SearchCriteria criteria);

} // More logic

public class CartPage{

SearchPage continueShopping(){

if(state) {

return new AdvancedSearchPage();

}

else {

return new HomePage();

}

}

And there’s more code and complex logic to automate a new user flow and modifications to CartPage . This violates the Open/Closed principle (OCP).

The open/closed principle(OCP) states “software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification”

Let’s keep it simple.

Note: The following examples use Gauge. In my other blog, I have discussed the top 5 reasons why I use Gauge.

Gauge tests are written in markdown. The steps define order of execution.

Loosely coupled steps makes creating a flow just a matter of choosing the right order of steps.

Steps See items available for purchase , Filter items by publication <publication> and continue shopping in the specification do not know about the previous step(s) nor the next step(s).

Here also we will have 2 classes, one for Simple Search and the other for the Advanced Search. But, the CartPage where the OCP violation occurs is not needed.

And yes, you can share information using DataStores.

Method chaining

Selenium recommends Page Objects returning other page objects.

Let’s discuss this with a simple Business Rule(BR) - login only with valid credentials.

Based on the BR, the user is either taken to LoginPage or the HomePage . Since both the pages are not similar, we cannot have a common return type. One way this to handle this is to have many methods for the same action.

public class LoginPage{

HomePage validLogin(String userName,String password){...}

LoginPage invalidLogin(String userName,String password){...}

}

The number of methods increase with more BRs e.g: role based login.

public class LoginPage{

HomePage validLogin(String userName,String password){...}

LoginPage invalidLogin(String userName,String password){...}

AdminDashboardPage adminValidLogin(String user,String pwd){...}

}

All this is because there is tight coupling between pages, the return type and actions.

Loosely coupled steps

Here the step Log in with customer name <name> and password <password> is not aware of the next step(s).

We have different test cases for positive and negative scenarios.

Test cases are the place to define order of steps and the required data.

Each step implementation, has code only to handle its task. Thus keeping the implementation loosely coupled.

Loosely coupled steps give users more flexibility and lesser code to maintain.