Reading:
Common UI Automation Patterns And Methodologies: Real-World Examples
Share:

Common UI Automation Patterns And Methodologies: Real-World Examples

How to adopt common software design patterns and methodologies to help with your UI automation and testing framework

Software development and software testing seem very different from each other at first glance, but there are aspects that are important in both disciplines.

In this article, we will check out some common software design patterns and methodologies that can be helpful when dealing with UI automation, especially with creating a UI testing framework. The examples and use cases mentioned here are related to our custom in-house framework.

Please note that these are not full-fledged examples but rather boiled-down code samples that illustrate the concepts. As it is my main programming language, the code below is written in Java. I’ve tried to keep it as simple and understandable as possible.

Helpful Patterns For UI Test Automation

A design pattern is not a finished piece of code that can be used in software projects. Instead, it is more like a blueprint for a solution.

A great advantage of using design patterns is that it simplifies communication with other developers and testers. If everyone is aware of the basic workings of a pattern, you simply need to mention its name to be on the same page instead of demonstrating your code in detail every time.

The Decorator Pattern

My organisation’s in-house testing framework needs to support different flavors of website components. This is necessary because our web application is constantly changing and A/B tests are performed at the component level.

If you have a similar requirement, the decorator pattern might be right for you! It makes it possible to pack components into "envelopes," which overwrite or supplement only certain functionalities. You do not have to write a new class for each new characteristic of a component: only the changes must be implemented. You can also use this technique if web components change depending on browser size or device type.

Decorator Example

In this basic example, we have two login components. The second one has an additional “cancel” button.

The conventional way to handle this would be to create classes for each component. The second would have the login functionality of the first plus the “cancel” functionality. However, creating more than one class leads to code duplication and disconnects each new component variation from its peers. It gets even more complex if you have multiple login components, for desktop and mobile for example.

Let’s look at it from the decorator perspective instead!
 

The LoginComponent is the interface for every login component. It states that each component needs to have a defined login method.

package decorator; public interface LoginComponent {   void login(String user, String password); }

The BasicLoginComponent has a concrete implementation of a login method. In this example, it just outputs “Basic login” on the command line.

package decorator; public class BasicLoginComponent implements LoginComponent {   @Override   public void login(String user, String password) {       System.out.println("Basic login: " + user + ", " + password);   } }

This class is the heart of the pattern. A LoginDecorator can take any LoginComponent and wrap some functionality around it. After this, the result remains a LoginComponent.

package decorator; public abstract class LoginDecorator implements LoginComponent {   private final LoginComponent loginComponent;   public LoginDecorator(LoginComponent loginComponent) {       this.loginComponent = loginComponent;   }   @Override   public void login(String user, String password) {       loginComponent.login(user, password);   } }

This MobileLoginDecorator overrides the basic login functionality with a new class that is specific to mobile. Again, it simply outputs “Mobile login” to keep this example short. 

package decorator; public class MobileLoginDecorator extends LoginDecorator {   public MobileLoginDecorator(LoginComponent loginComponent) {       super(loginComponent);   }   @Override   public void login(String user, String password) {       System.out.println("Mobile login: " + user + ", " + password);   } }

This CancelButtonDecorator can add the cancel functionality to any login component.

package decorator; public class CancelButtonDecorator extends LoginDecorator {   public CancelButtonDecorator(LoginComponent loginComponent) {       super(loginComponent);   }   public void cancel() {       System.out.println("Click the cancel button");   } }

This is what our final class structure looks like:

Now we can test how it all behaves!

package decorator; public class Main {   public static void main(String[] args) {       System.out.println("DECORATOR PATTERN");       System.out.println("=================");       // This is the basic login component       LoginComponent loginComponent = new BasicLoginComponent();       loginComponent.login("User", "PW");       // Let's turn it into a mobile login component.       loginComponent = new MobileLoginDecorator(loginComponent);       loginComponent.login("User", "PW");       // Finally, we can add a cancel functionality.       loginComponent = new CancelButtonDecorator(loginComponent);       ((CancelButtonDecorator) loginComponent).cancel();   } }

The output of it all is:

DECORATOR PATTERN ================= Basic login: User, PW Mobile login: User, PW Click the cancel button

Even though this seems to be a lot of overhead at first, it is less work in the long run. You  need only to implement the changed or added functionality in a decorator and add it to the component you want to change. This keeps your test code concise and modular.

Page Objects And Page Components

One of the first patterns specific to UI automation is the page object pattern. This means that all functionality of a specific web UI is wrapped into a class. This is good for simple views without a lot of interaction possibilities as the page objects are clear and manageable.

However, if a specific page contains a lot of functionality, page object classes can get huge and turn into complex and chaotic code. This is where an extension of page objects comes in: page components.The idea is to wrap a component’s functionality into a class instead of the whole page.

Page Object Example


This is a very basic web shop that includes a search and a result list of found products. If you implement this using a page object, the result could look something like this WebshopPage class.

package pageobjects; public class WebshopPage {   public void search(final String queryString) {       System.out.println("Enter " + queryString);       System.out.println("Click search button");   }   public void checkResultHeadline() {       System.out.println("Check if the headline is correct.");   }   public void checkResults() {       System.out.println("Check if there are search results.");   } }

Each action that can be performed on this page is included here. We can test this using a simple main class.

package pageobjects; public class Main {   public static void main(String[] args) {       System.out.println("PAGE OBJECTS");       System.out.println("============");       WebshopPage webshopPage = new WebshopPage();       webshopPage.search("T-Shirt");       webshopPage.checkResultHeadline();       webshopPage.checkResults();   } }

As expected, this gives us the following output:

PAGE OBJECTS ============ Enter T-Shirt Click search button Check if the headline is correct. Check if there are search results.

For now, this approach is OK. But if more functionality is added to this page, the test code could quickly get messy.

Page Components Example

This is where page components come in. In our case, you could separate the page into two components: a search bar and a result list.

The SearchBar class needs to contain only the search method.

package pagecomponents; public class SearchBar {   public void search(final String queryString) {       System.out.println("Enter " + queryString);       System.out.println("Click search button");   } }

The methods for checking the result headline and the results themselves belong to the ResultList:

package pagecomponents; public class ResultList {   public void checkResultHeadline() {       System.out.println("Check if the headline is correct.");   }   public void checkResults() {       System.out.println("Check if there are search results.");   } }

There is still a WebshopPage but this version simply accesses the two components.

package pagecomponents; public class WebshopPage {   public SearchBar searchBar() {       return new SearchBar();   }   public ResultList resultList() {       return new ResultList();   } }

We need to make only some slight changes to the example. Instead of accessing the functions through the page, now we can do it through the components.

package pagecomponents; public class Main {   public static void main(String[] args) {       System.out.println("PAGE COMPONENTS");       System.out.println("===============");       WebshopPage webshopPage = new WebshopPage();       webshopPage.searchBar().search("T-Shirt");       webshopPage.resultList().checkResultHeadline();       webshopPage.resultList().checkResults();   } }

The result is still the same:

PAGE COMPONENTS =============== Enter T-Shirt Click search button Check if the headline is correct. Check if there are search results.

This solution requires more classes but there is much less code in each, making this easier to understand and maintain. It also allows for better reusability of each component in case the component appears on more than one page.

The Factory Pattern

In general, especially when using the page component pattern mentioned above, you must create many different classes when testing an application. The obvious approach is simply to create a new instance of each class you need and use those. 

There is a major drawback to this approach, though. As every component class must be known by the framework, each component needs to share duplicated initialisation code when it is created. With a factory class, you can implement a single source of truth that can produce instances of every needed class.

Factory Pattern Example

For this example, we can extend the page component example above and use a factory to create the components.

First, we need a class from which all of our components inherit, so all have an initialisation method. We could also use an interface if each future component might have a different initialisation method. For simplicity’s sake, let’s assume initialisation is the same for all.

package factory; public class Component {   public void initialize() {       System.out.println("Initializing " + getClass().getName());   } }

Each of the components above can inherit from this Component class:

public class ResultList extends Component {    ... }public class SearchBar extends Component {    ... }

The missing piece is the factory class that is in charge of producing components. For illustration purposes, the factory class takes a component name as an argument, creates an instance of the according class, calls its initialize method and returns it back.

package factory; public class ComponentFactory {   public static Component getComponent(final String componentName) throws Exception {       System.out.println("Creating " + componentName + "...");       // Create a component instance for the passed in component name.       Component component;       switch (componentName){           case "SearchBar":               component = new SearchBar();               break;           case "ResultList":               component = new ResultList();               break;           default:               throw new Exception(componentName + " unknown.");       }       System.out.println("Component created: " + component);       component.initialize();       return component;   } }

We can now modify the WebshopPage class so it uses the factory to return its components.

package factory; public class WebshopPage {    public SearchBar searchBar() throws Exception {        return (SearchBar) ComponentFactory.getComponent("SearchBar");    }    public ResultList resultList() throws Exception {        return (ResultList) ComponentFactory.getComponent("ResultList");    } }

 

The code of the main class does not look different because the WebshopPage is still in charge of managing its components.

package factory; public class Main {   public static void main(String[] args) throws Exception {       System.out.println("FACTORY PATTERN");       System.out.println("===============");       WebshopPage webshopPage = new WebshopPage();       webshopPage.searchBar().search("Berlin");   } }

This is the output of the modified example:

FACTORY PATTERN =============== Creating SearchBar... Component created: factory.SearchBar@3d075dc0 Initializing factory.SearchBar Enter Berlin Click search button

The component is requested, created, and initialised as expected. 

Like the examples before, there are more classes now. The advantage is that there is a centralised place for retrieving and initialising components, which makes development more efficient if future components have to be implemented. 

Dependency Injection

This pattern is derived from the idea of "Inversion of Control." In this pattern, objects receive other objects they need from the outside instead of creating them on their own. This simplifies the construction of nested classes and makes them much easier to unit test. 

Typically, software that uses dependency injection relies on some specialised framework like Spring or Guice that handles object creation and injection. To clarify the concept, the following example does not use a framework.

Dependency Injection Example

Here, we want to provide some login functionality for a UI test of a simple website. However, we want to store the username and password directly inside a class so we don’t need to pass it whenever we want to invoke a login. Also, our requirement is that we can have multiple sets of username and password depending on the test case. This new requirement keeps us from simply including the login data inside of the login page, as it has to remain flexible. That’s why “injecting” the data from the outside is a good choice.

This is the LoginData interface that all of our login data instances should implement. It simply returns a username and password.

package dependencyinjection; public interface LoginData {   String getUserName();   String getPassword(); }

Let’s review two implementations, one for “real” and one for “fake” login data.

package dependencyinjection; public class LoginDataReal implements LoginData {   @Override   public String getUserName() {       return "Real user";   }   @Override   public String getPassword() {       return "Real password";   } } package dependencyinjection; public class LoginDataFake implements LoginData {   @Override   public String getUserName() {       return "Fake user";   }   @Override   public String getPassword() {       return "Fake password";   } }

The LoginPage constructor accepts an instance of a LoginData class and uses it in its login method. So the actual username and password to use is not managed by the login page itself, but instead is chosen and injected from the outside.

package dependencyinjection; public class LoginPage {   private final LoginData loginData;   public LoginPage(final LoginData loginData) {       this.loginData = loginData;   }   public void login(){       System.out.println("Logging in with " + loginData.getClass());       System.out.println("- user: " + loginData.getUserName());       System.out.println("- password: " + loginData.getPassword());   } }

The only missing part is a test class that uses both sets of login data.

package dependencyinjection; public class Main {   public static void main(String[] args) {       System.out.println("DEPENDENCY INJECTION");       System.out.println("====================");       LoginPage loginPageReal = new LoginPage(new LoginDataReal());       loginPageReal.login();       LoginPage loginPageFake = new LoginPage(new LoginDataFake());       loginPageFake.login();   } }

This class creates two separate login pages that differ only in the passed-in login data. Running the class prints the following:

DEPENDENCY INJECTION ==================== Logging in with class dependencyinjection.LoginDataReal - user: Real user - password: Real password Logging in with class dependencyinjection.LoginDataFake - user: Fake user - password: Fake password

Again, this solution requires more classes, but there is much less code in each, making it easier to understand and maintain. It also allows for better reusability of each component in case it appears on more than one page.

You can see that this makes our software more flexible as it is simple to rewire its components without having to change any code in most of the classes. Also, it leads to more testable code because each class can be tested as an individual unit.

Finally: Two Methodologies

We just looked at a lot of code. So let’s wrap up this article with something completely different: methodologies!

Whenever you need to write or extend a UI test framework, it is handy to stick to some guidelines that help you implement the right things for your customers or teammates. Personally, I always try to stick to the examples below. These have proven to be very helpful for me, keeping me from getting lost in the process.

Keep It Simple

Simple systems work better than complicated ones. This principle does not apply only to the inner workings of software but also to the end user interface. In case of a UI test framework, writing a test should be as straightforward as possible. After all, one of the key concepts behind any framework is simplification of more complex tasks.

You Aren’t Gonna Need It

For me personally, this principle, also known as YAGNI, is one of the most important. It is related to “keep it simple” in the sense that you should do the simplest thing that could possibly work. This means that new features should be added only when you are sure they are needed, not because you think they will be needed at some point. 

If you stick to this principle, you will probably develop your test framework faster, because you don’t need to think about all possibilities before implementation. Also, you can deliver exactly the tools that are needed on request instead of providing a ready-made solution that may be overloaded and complicated to use.

Further Reading

For more information on design patterns, see “Design Patterns: Elements of Reusable Object-Oriented Software” by Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides (1994).

For some cross-disciplinary ideas on how to keep process debt to a minimum, see When Testers Deal With Process Debt: Ideas to Manage It And Get Back To Testing Faster

Author Bio

After being a game/application developer and trainer for 15 years, Benjamin decided in 2017 to make test automation his main career. He works as a Test Automation Engineer in trivago's core QA team. His focus is the development and maintenance of their in-house end-to-end test framework and related build pipelines. More information about him can be found on his website: https://www.softwaretester.blog/

 

Benjamin Bischoff's profile
Benjamin Bischoff

Test Automation Engineer

Test Automation Engineer @ trivago, speaker and magician



How To Win With Automation And Influence People - Gwen Diagram
Testing Ask Me Anything - Technical Testing
Approach to Comparing Tools with Matthew Churcher
"Are we there yet? Driving quality & tackling automation debt" with Amber Pollack-Berti
Gaining Confidence with Cypress Tests
End-to-end web testing - TestCafé - Julian M Bucknall
Pairing With Developers: A Guide For Testers
My Journey to Becoming a DevOps Tester... By Playing Board Games - Hannah Beech
Is Acceptance Test Driven Development (ATDD) Worth the Effort?
Selenium 4 introduces relative locators. This new feature allows the user to locate an object in relation to another object on the screen! Don't wait, get an instant demo today.
Explore MoT
Episode One: The Companion
A free monthly virtual software testing community gathering
MoT Advanced Certificate in Test Automation
Ascend to leadership roles by mastering strategic skills in automation strategy creation, planning and execution