Engineering

Top design patterns for test automation frameworks

The pitfalls of poor automation testing frameworks 

A poorly designed architecture is a major reason why test automation frameworks fail. Engineers need to identify problems and adopt the right design patterns upfront. Common factors that result in bad design are: 

  • Those implementing the work are new to or unfamiliar with test automation tactics. 
  • The project’s timeline is strict. 
  • The scale is unclear.  
  • Engineers lack basic programming knowledge and, as a result, don’t use the right principles.  

There is a fix. Take a moment to identify blockers or problems. Once the issues are identified, it’s easier for engineers to build maintainable, extendible frameworks. 

Design patterns: A better software testing technique

When designing an application, developers need to solve problems along the way. There is a good chance that another developer already solved the exact same or similar problem already. Design patterns provide a general reusable solution for the common problems that occur in software design. Design patterns are like collections of best practices as they provide a concept but not particular implementations. Design patterns help reduce code complexity—as well as make code more extensible, and maintainable. 

Design patterns are grouped into three categories:  

  • Structural patterns explain how to assemble objects and classes into larger structures while keeping these structures flexible and efficient. 
  • Creational patterns provide various object creation mechanisms, which increase flexibility and reuse of existing code. 
  • Behavioral patterns are concerned with algorithms and the assignment of responsibilities between objects. 

In this article, I’ll go over the most popular structural, creational, and behavioral design patterns, outlining what they are, why they’re useful, and how to structure them. 

Before diving into design patterns, it’s important to become familiar with SOLID principles that help make software design more understandable, flexible, and maintainable. 

  • The Single Responsibility Principle - A class should have one, and only one reason to change. 
  • The Open Closed Principle - You should be able to extend a class behavior without modifying it. 
  • The Liskov Substitution Principle - Derived classes must be substitutable for its base classes. 
  • The Interface Segregation Principle - Make fine grained interfaces that are client-specific. 
  • The Dependency Inversion Principle - Depend on abstractions, not on concretions. 

Structural design patterns 

Page Object Models (POM) 

What are they? 

This is the most popular structural design pattern and is commonly used in building test automation frameworks to automate UI test cases. The pattern abstracts any page information away from the actual tests.  

Why are they useful? 

POM’s are beneficial because: 

  • Developers don't need to write duplicate code. They create page components (e.g., input fields, buttons, etc.) which are used when building other page objects.  
  • The pattern provides encapsulation and abstraction. Page objects encapsulate web elements, or other class members and provide access to wrapper methods that hide all the logic (i.e., the mechanics used to locate or change data).  
  • Creating and organizing page objects makes it easier for developers to understand the structure of the automation solution via usage, new test creations, creation of new page components, and readability of tests. 

How are they structured? 

Page objects are like interfaces for web pages or web page components, also known as loadable components. These components support the same interaction with other web pages and should consist of: 

  • preferably encapsulated web elements 
  • other page objects 
  • methods to operate with page object and return other page objects by design 
  • no assertions should be used on page objects  

To implement POM effectively, follow a folder and file structure to organize and keep page objects easily accessible. Page objects can be implemented in a functional or structural manner, depending on the approach and need. 

Composite patterns 

What are they? 

This pattern composes objects into tree structures to represent part-whole hierarchies. Composites let clients treat individual objects and compositions of objects uniformly. 

Why are they useful? 

A composite pattern is beneficial because: 

  • The simplified representation of a complex tree structures components as branches with the help of polymorphism and recursion. 
  • Implementing new items to the structure without altering the code (Open/Close principle) is easy. 

How are they structured? 

Composite patterns in automation solutions stem from a collection of page objects built up, forming a tree structure. While true, the main point of a composite pattern is to treat individual objects and compositions of objects uniformly. The structure gives the client control to operate through abstract the component interface. 

Engineers use a composite pattern to create components that benefit from a tree structure (e.g., menus needing sub-menus, sub-sub-menus, etc.) to get, select, or print all the items via recursion. For more complex needs, include custom iterators. 

In the image above, the TreeItem could be implemented as an interface or as an abstract class (if one would like to add default methods implementation). TreeLeaf is a basic element with most of the time all the code “meat” lives in these elements and does not feature other elements. TreeBranches hold other TreeItems and delegate all the work to child elements (i.e., tree leaves). 

Facade patterns 

What are they? 

The facade pattern provides a unified interface to a set of interfaces in a subsystem. The facade defines a higher-level interface that makes the subsystem easier to use. 

Why are they used? 

A façade pattern is beneficial because: 

  • Developers are able to create structure, consisting of layers of subsystems. 
  • Developers are able to simplify/hide code complexity from consumers.  

How are they structured? 

Facade is another structural design pattern that helps hide the complexity of one or more classes and provide one or more reasonable interfaces. The pattern still leaves direct access to the complex classes in case the user needs advanced functionality. Besides simplifying the interface, a facade pattern also decouples the client from inner system components. 

In test automation, a facade is mostly used to combine a few page objects/actions and provide uniform actions for consumers. For example, when a complex API needs to be executed in a specific order, create a facade for the designated functionality and provide a simplified interface for operating. 

Decorator patterns 

What are they? 

This pattern attaches additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality. 

Why are they useful? 

A decorator pattern is beneficial because: 

  • Developers are able to add new functionality to already existing objects without modifying its code (Open/Closed Principle). 
  • Developers are able to follow the single-responsibility principle allowing them to have multiple smaller classes divided by functionality.

How are they structured? 

Decorators attach new behaviors to objects by placing the objects inside special wrapper objects that contain new behaviors. 

For example, to add logging functionality without a lot of changes to test automation solution using Webdriver, simply wrap (i.e., decorate) the Webdriver into EventFiringWebDriver. IWebdriver, IWebElement, INavigation, IOptions, ITargetLocator, and ITimeouts wrap with multiple new responsibilities. As a result, EventFiringWebDriver fires events on the main actions attached and listen to.  

With EventFiringWebDriver implemented, engineers easily access screenshots and logging on every exception or other action. To assemble: 

  1. Create a listener class. 
  2. Implement a method to capture screenshot and logging. 
  3. Call that method on a specific event. 
  4. Register the event listener to EventFiringWebDriver. 

Creational design patterns 

Factory method patterns 

What are they? 

This pattern defines an interface for creating an object but allows subclasses to decide which class to instantiate. Factory methods let a class defer instantiation to subclasses. 

Why are they useful? 

A factory method pattern is beneficial because: 

  • Having all objects initialization in a single place makes support much easier later (Single Responsibility Principle). 
  • The pattern is easily extendable without altering existing code (e.g., open/close principle). 
  • Scalability is easy to handle for tests runs (e.g., using Selenium Grid/ Docker containers).    

How are they structured? 

A factory method pattern is a creational pattern used for encapsulating the instantiation of concrete types, avoiding tight coupling between the creator and concrete products. Engineers don’t need to modify any code to extend the codebase.  

One of the best examples of a test automation framework is with the Selenium WebDriver initialization. While most of the initializations require some kind of setup afterward, when all Webdrivers share the same "IWebDriver" interface, refer Webdrivers through that interface. Adding another Webdriver (if necessary) is easy. Simply extend the factory method, which does not modify already existing code.  

The Webdriver includes a builtin factory method called PageFactory responsible for all page element initialization. To implement, declare the web elements and provide additional attributes if needed. 

Builder patterns 

What are they? 

This creational design pattern lets developers construct complex objects step by step. The pattern produces different types and representations of an object using the same construction code. 

Why are they useful? 

A builder pattern is beneficial because: 

  • Developers are able to create multiple representations of an object. 
  • The pattern isolates complex and the repetitive construction code in an object. 

How are they structured? 

As with any automation solution for software, a builder pattern is used to create an object needing many different representations. Representations with telescopic constructors are hard to maintain with difficulties generating readable code. Use the builder pattern to create fluent page objects. A builder pattern is easy to implement and understand. 

For example, when orchestrating a data transfer objects (DTO), like permissions for an object, with permissions such as read, create, update, and delete or any other object permission that needs to be built dynamically. 

When engineers need to build already predefined objects, consider introducing a director-class responsible for wrapping the builder defined steps in some kind of order (e.g., buildCRUD(Builder builder) which calls addCreate(), addRead(), addUpdate(), and addDelete() builder steps). 

Singleton patterns 

What are they? 

This pattern ensures a class has only one instance and provides a global point of access. 

Why are they useful? 

A singleton pattern is beneficial because: 

  • There’s only one instance of an object. In most cases, the pattern is used for Logger, Connections, or External Resources. 
  • Developers need a global access point to a class instance. 

How are they structured? 

 

 

Behavioral design patterns 

Strategy patterns 

What are they? 

This pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. Strategy patterns let the algorithm vary independently from the clients that use it. 

Why are they used? 

A strategy pattern is beneficial because:  

  • Engineers have various algorithms to complete a task, and that can switch depending on the specific task at the run time. 
  • Developers are able to easily add new strategies without altering the code (Open/Close principle). 

How are they structured? 

In test automation, strategy patterns are useful in various fields. For example, an application includes multiple payment methods on a checkout page or a different implementation on the same action, like account creation. One pattern uses UI, and another uses RESTful calls.  

These algorithms (strategies) are easily defined, encapsulated, and used for various needs. Engineers take all the logic from page objects and lets page objects delegate all the work to the strategy objects. 

Summary

Design patterns and its application is an exceptionally worthwhile investment, despite the initial learning curve. 

To ease and speed up development test automation: 

  • Constantly evaluate and identify potential issues. 
  • Review the current design before implementing a new feature  
  • Keep engineers up to date on current objectives and future plans. 
  • Document most of the agreements on paper 
  • Have a backlog and actively work on corresponding tasks. 

Take time to understand the problem at hand and what design pattern solves the problem. Remember, constantly evaluate and identify problems to keep code clean. Needs change. Be mindful. Add a pattern when there’s a practical need for one. Sometimes the simplest solution is no pattern at all. Adding a pattern without just cause adds unnecessary complexity to an automation solution. When designing, address problems in the simplest way possible. Just keep it simple.