Design Patterns in Test Automation
May 18, 2023

An Overview of Software Design Patterns & Test Automation

DevOps
Test Automation

In today's rapidly evolving world of technology, software development has emerged as a central pillar of innovation and progress. According to the US Bureau of Economic Analysis, the US digital economy, which is driven by software, accounted for $3.7 trillion of gross output in 2021 and created millions of jobs. This is a massive achievement for a relatively young discipline compared to other fields like mathematics and physics. 

However, with this exponential growth comes the need for structure and guidelines that can improve the quality of core software development. Enter software design patterns to bridge the gap in the landscape of software development. Design patterns offer a much-needed boost to core software development by providing high-level guidelines and best practices.

Modern software developers write many types of code, for example:

  1. Application code
  2. Test automation code
  3. Infrastructure as code

Using software design patterns can improve the quality of all types of code, including test automation code. In this blog, we will go through the basics of design patterns and how they are related to test automation. Following are the key points tackled by this post.

  • What are software design patterns?
  • Design patterns categories
  • Advantages of using design patterns
  • The iterator design pattern in Java
  • Using test automation for software design patterns
    • Page Object Model
    • Use of Factory Pattern in Test Automation
    • Example of Using Factory Method Pattern in Test Automation
  • Use of the singleton pattern in test automation

I hope you find this blog useful!

Back to top

What are Software Design Patterns?

Software design patterns are approaches and best practices to software design that can be helpful when designing and developing applications and systems. They are like maps and guidelines that help developers avoid common pitfalls related to specific scenarios. Usually, they are expressed as a definition and a UML (Unified Modeling Language) diagram.

Two types of sources inform design patterns:

  • The experiences of software developers who've already faced similar problems
  • Fundamental software design principles

Design patterns provide us with packaged design experience and insights and offer solutions for some of the most well-known software design problems. Software developers and testers can study and use design patterns in their code to solve some prominent issues. The advantages of using design patterns are explained in this post. Some examples of using design patterns in code are given further down in this blog.

Back to top

The Origins of Software Design Patterns

The concept of design patterns was first established in the 1994 book “Design Patterns: Elements of Reusable Object-Oriented Software” written by the famous “Gang of Four” (Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides). A total of 23 design patterns were  present in this software engineering classic.

Back to top

Is It Possible To Use Software Design Patterns in Automated Testing?

Yes, it is possible to use design patterns in your test automation scripts. Design patterns like page object model, factory model, and singleton are baked into popular test automation libraries like Selenium and RestAssured. You can use any design patterns to streamline your test code to make it more robust and maintainable.

Back to top

Software Design Patterns Categories

Design patterns are divided into three categories:

  1. Creational design patterns: These design patterns are focused on object creation mechanisms. They provide standardized solutions to common problems that arise during object creation, making it easier to create new objects and modify existing ones.
  2. Structural design patterns: These patterns are concerned with the composition and structure of classes and objects. They focus on creating relationships between objects flexibly and efficiently. Structural design patterns help developers build complex systems by providing standardized solutions to common design problems. These patterns are often used to organize code in a way that is easy to understand and maintain.
  3. Behavioral design patterns: These patterns are concerned with communication between objects and assigning responsibilities between them. They focus on defining how objects interact and handle tasks or actions. They are often used to improve communication between objects and simplify the codebase by reducing the amount of hard-coded logic.

The original “Gang of Four” book contained twenty-three design patterns. A list of design patterns by category is given below. 

Creational Patterns

Structural Patterns

Behavioral Patterns

Abstract Factory Builder

Adapter

Chain of Responsibility

Builder

Bridge

Command

Factory Method

Composite

Interpreter

Prototype

Decorator

Iterator

Singleton

Facade

Mediator

 

Flyweight

Memento

 

Proxy

Observer

 

Page Object Model

State

  

Strategy

  

Template

 

  

Visitor

Back to top

Software Design Patterns and Object-Oriented Design Principles

Although this article is about software design patterns, it is important to understand object-oriented design principles, because the concept of design patterns was first described in the book “Design Patterns Elements of Reusable Object-Oriented Software Design.” Ever since, design principles are considered general guidelines, but patterns are specific design solutions for common object-oriented problems.

Design patterns help developers avoid common software design pitfalls in specific design situations. Developers create design patterns by following the Object Oriented (OO) Design principles. OO design principles are key in improving software quality created with OO programming languages. The essential object-oriented design principles are listed below:

  • Single Responsibility
  • Open-closed principle
  • Loose coupling
  • Using composition rather than inheritance
  • Programming to an interface, not an implementation
  • Encapsulation
  • Encapsulating what varies
  • Abstraction
  • Polymorphism
  • Inheritance

A detailed explanation of these concepts is beyond the scope of this blog, but a mention is essential for clarifying concepts.

Back to top

Advantages of Using Software Design Patterns

There are many advantages to using software design patterns. These include:

  1. Using design patterns helps avoid “reinventing the wheel”. It shortcuts the development process by leveraging the hard work of other developers who’ve already undergone a similar exercise. One example I will show in this blog is by using a combination of the Selenium page factory @FindBy tag and an extended for loop in Java.
  2. Design patterns protect us from common design pitfalls and help us write resilient code.
  3. When we use design patterns correctly, they help improve software quality by reducing the number of bugs.
  4. Software design patterns save time and effort by facilitating maintainability and reusability.
  5. Many patterns, such as the iterator, factory, and strategy patterns, are based on the OO design principle of “encapsulate what varies.” Such separation leads to a more flexible and resilient software design. Therefore, software design issues can be solved flexibly using design patterns. They enable software modifications without affecting the entire system.
  6. Design patterns provide scalable solutions to software design problems. They allow for the software to grow and adapt in an agile way as the needs of the system change.
  7. Design patterns provide a standardized approach to software design and help ensure the code is consistent and adheres to best practices.
  8. Design patterns offer a shared vocabulary for discussing software design because they are well-known and comprehended. It is simpler for developers to communicate about the code using design patterns.
Back to top

The Iterator Design Pattern (and Its Inclusion in the Java Language)

Iteration is the process of traversing an aggregate object. The iterator design pattern simplifies the traversal of a collection, such as an array. It offers a method for sequentially accessing an aggregate object's elements without revealing its underlying representation, allowing your code to handle a variety of aggregate objects without needing a change in iteration code. Some examples of aggregate objects include

  • Arrays
  • Lists
  • Sets
  • Maps
  • Dictionaries

When the iterator pattern is used, all aggregate objects provide an iterator. The iterator an aggregate provides knows how to iterate over a specific aggregate. Your code only needs to ask for an iterator and use it. All iterators provide a method like getIterator,  createIterator, or simply iterator to retrieve an instance of an aggregate object’s iterator. 

Once you have an instance of an iterator, you can use any loop to iterate through an aggregate object.

Over time, design patterns have become an integral part of modern programming languages. For example, Java language contains an “enhanced for loop.” The iterator pattern is the foundation of the “enhanced for loop,” which means the iterator pattern is also a part of the Java language. A diagram explaining the iterator design pattern is given below.

Example: Iterator Pattern in a Test Method

Below is an example that shows multiple ways to use the iterator design pattern in Java. 

@Test
public void testIteratorPattern() throws InterruptedException
{
   ArrayList<Integer> list = new ArrayList<Integer>(10);
   for(var i = 0; i < 10; i++)
   {
       list.add(new Random().nextInt());
   }

   //Example 1. Using an iterator to traverse a list
   for (var itr = list.iterator(); itr.hasNext(); )
   {
       out.printf("Iterator number is: %s\n", itr.next());
   }

   //Example 2. Using an iterator with extended for loop
   for(var val:list)
   {
       out.printf("Extended for number is: %s\n", val);
   }

   //Example 3. Using a lambda in Java 8 and above to iterate through a list
   list.forEach(v -> System.out.printf("Lambda number is: %s\n", v));
}

This test method first creates an ArrayList of Integer values and populates it with random integers. 

After the initialization, three different techniques for traversing a collection are shown. 

Example 1 shows how to use the iterator design pattern to traverse an array with a traditional for loop. 

Example 2 shows how to use an iterator with an extended for loop. C# also provides a similar loop known as the foreach loop. 

Example 3 shows how to use a lambda function to perform a specific operation on each value stored in a container or aggregator.

Use of Iterator Design Pattern in Test Automation

It is possible to use the iterator design pattern in test automation scripts. You can see one such example listed below. This Java example demonstrates the combination of the factory pattern and the iterator pattern.

@FindBy(how= How.XPATH, using = "//a[contains(text(), 'BlazeMeter')]")
List<WebElement> searchResults;
public void printSearchResults()
{
   for (var s: searchResults
        ) {
       System.out.println(s.getText());
   }
}

The @FindBy tag is used to locate all hyperlinks on a web page that contain the text ‘BlazeMeter’. The instantiation of the list named searchResults is handled automatically by the Selenium PageFactory class.

In the above example, the extended for loop of Java language is used to traverse the search results list created by a PageFactory.

Back to top

Software Design Patterns in Test Automation

Software test automation projects can benefit tremendously from using design patterns. Some design patterns have evolved specifically to cater to the needs of test automation. The most popular design pattern used in test automation is Page Object Model. Additional design patterns are also used in test automation, for example, Bot Pattern and Page Factory pattern. Here is a list of commonly used design patterns in test automation:

  • Page Object Model
  • Factory Pattern
  • Singleton Pattern
  • Bot Pattern
  • Facade Pattern
  • Decorator Pattern
  • Iterator Pattern

The first three are the most commonly used. Below is a detailed description of the page object model, factory pattern, and singleton pattern. 

Page Object Model

The Page Object Model, or POM, is a prevalent design pattern, which has its roots in end-to-end testing of websites. It is often used to build custom test automation frameworks. Page Object Model uses encapsulation to separate test code and the code needed to perform various actions on a system under test. Please don’t confuse it with the Project Object Model used in Maven projects.

The sections of the website under test are divided into page classes. Each page class contains two things:

  • Locators needed to access the UI elements on a page
  • Methods for performing actions on a given page

Any test can instantiate any number of page objects needed to perform the arrange, act, and assert steps of a test scenario. The sample diagram given below shows how the Page Object Model works.

As you can see, multiple tests can reuse the page objects created to test a sample application. The page object model allows us to reuse code. If the website UI has changed slightly, the tests don’t need to be updated. Instead, the pages will be updated or modified to accommodate the changes. 

Page Object Model in Test Automation with Selenium

The Selenium Support classes package comes with a PageFactory class that is very helpful in creating page object models for use in Selenium end-to-end testing. We only need to derive our page classes from the Selenium PageFactory class to leverage the power of the page factory. The page class constructor only needs to call the method initElements to make the page class work. 

When we derive a page class from Selenium PageFactory, we can use annotations like @FindBy, @FindAll, and @FindBys.

The code below shows a working example of the page object model by providing the source code of a page class created using Selenium PageFactory. 

public class PageGoogle extends PageFactory {
   private final WebDriver driver;
   @FindBy(how= How.NAME, using = "q")
   public WebElement searchField;

   @FindBy(how=How.PARTIAL_LINK_TEXT
, using="BlazeMeter Continuous Testing | BlazeMeter by Perforce")
   public WebElement searchResult;

   public PageGoogle(WebDriver driver)
   {
       this.driver = driver;
       initElements(driver, this);
   }

   public void searchGoogle(String searchTerm)
   {
       searchField.sendKeys(searchTerm);
       searchField.submit();
   }

   public boolean isSearchResultFound()
   {
       return new WebDriverWait(driver, Duration.ofSeconds(5)).
               until(ExpectedConditions.visibilityOf(searchResult)) != null;
   }
}

Use of Factory Pattern in Test Automation

The factory pattern allows us to decouple the process of creating objects from the clients that use those objects. In the case of test automation, the “client” will be your test classes and methods. We’ve already provided one example of using the factory pattern while explaining the Page Object Model using PageFactory. The Selenium PageFactory class frees our page objects from creating WebElement objects needed to perform operations on various page sections.

It is also possible to use other variations of the factory pattern in test automation. For example, we can use the Factory Method pattern to create Page Objects for use in our test methods.

Example of Using Factory Method Pattern in Test Automation

The Factory Method pattern defines an interface for creating an object, while allowing subclasses to choose which class to instantiate. The factory method pattern allows a class to delegate instantiation to subclasses. Since design patterns only provide guidelines, it is possible to implement the factory method in multiple ways.

For example, if we have a test method that performs the following steps:

  1. Search ‘BlazeMeter’ on Google
  2. Check for search results on Google
  3. Open a search result
  4. Verify that the BlazeMeter is opened

The code for such a method is below. I have omitted some details for brevity's sake.

@Test
public void googleBlazeMeterTest()
{
   PageGoogle google = new PageGoogle(driver);
   google.searchGoogle("BlazeMeter");
   google.isSearchResultFound();
   google.openSearchResult();
   PageBlazeMeter blaze = new PageBlazeMeter(driver);
   blaze.isBlazeMeterPageOpened();
}

Here, you can see that the test method instantiates the page objects. Using this approach leads to tight coupling. It is possible to delegate the task to a page factory class instead. By introducing a page maker factory class, the test code will look like the following:

@Test
public void googleBlazeMeterTest()
{
   var google = PageMakerFactory.getGooglePage(driver);
   google.searchGoogle("BlazeMeter");
   google.isSearchResultFound();
   google.openSearchResult();

   var blaze = PageMakerFactory.getBlazeMeterPage(driver);
   blaze.isBlazeMeterPageOpened();
}

We introduced a new class PageMakerFactory that exposes some static methods for creating page objects. I’ve written the straightforward code of our PageMakerFactory below:

public class PageMakerFactory {
   public static PageGoogle getGooglePage(WebDriver driver) {
       return new PageGoogle(driver);
   }
   public static PageBlazeMeter getBlazeMeterPage(WebDriver driver) {
       return new PageBlazeMeter(driver);
   }
}

We can use more sophisticated techniques Java Generics and Reflection to create a generic method to instantiate multiple types of page objects. But demonstrating such a technique is going to complicate matters for new learners.

Use of Singleton Pattern in Test Automation

The singleton design pattern is a creational design pattern that limits a class's instantiation to one instance and provides a global access point. This pattern is useful when managing shared resources or creating a centralized object for coordinating actions across an application. You need to ensure that only one instance of a class exists. You can avoid synchronization, memory usage, and consistency issues by ensuring that only one class instance exists, making your code more efficient and reliable.

The singleton pattern can come in handy with many scenarios. For example, in API testing scenarios.

Please see an example implementation of a singleton class in Java below.

public class OTPAPI {

   private static final OTPAPI instance = new OTPAPI();

   // Singleton uses a private constructor to avoid client applications instantiating an object
   private OTPAPI(){}

   public static OTPAPI getInstance() {
       return instance;
   }
}

You might be thinking ‘Why not just use static methods as shown in the PageMakerFactory example?’ When you need to maintain state or share resources, using the singleton pattern rather than static methods is best. Singletons are more flexible and testable than static methods because they allow you to inject dependencies and mock objects for testing. Furthermore, using a singleton can help to make your code more modular and easier to maintain over time.

Back to top

Conclusion

Design patterns, object-oriented design, and test automation are vast subjects about which many books have been written. In this article, we tried to introduce you to the best available options in the shortest and easiest possible time. Now you have some knowledge about software design patterns in test automation that you can carry with you in your testing endeavors.

There is no better partner than BlazeMeter to conduct automated testing of your software design patterns. See for yourself by trying BlazeMeter for free.

Start Testing Now

Back to top