Singleton Design Pattern:
How to Use It In Test Automation

In this article, you will learn about the Singleton pattern and, more importantly, how to apply it in a test automation framework. We will be using Java and Selenium WebDriver for demonstration purposes. However, you may use a different programming language or test automation tool to accomplish the same.

 

I am a Lead Software Developer Engineer in Test (SDET) with over nine years of software testing experience in the e-commerce, engineering, insurance, and workforce intelligence domains. My experience consists of leading teams of varying sizes, and I possess vast experience testing web and mobile applications, APIs, load and performance, and much more. I am also a Selenium Conference speaker, coach, influencer, and blogger on all things software testing. Helping others succeed makes me happy. For this reason, I founded automateNow, and my main goal is to help as many aspiring engineers as possible to make their dreams a reality.
In my spare time, I enjoy running, biking, hiking, and working on cars. As well as adding to my book collection on history, philosophy, religion, finance, and other personal development books.

We will be using Java and Selenium WebDriver for demonstration purposes. However, you may use a different programming language or test automation tool to accomplish the same.

Design patterns history

A team of four computer scientists (Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides) known as The Gang of Four (GoF) introduced what we now know as software design patterns back in the mid-1990s.

We can use these design patterns to develop reusable object-oriented software.

Moreover, Christopher Alexander, an Austrian-born British-American architect and design theorist, and his colleagues may have been the first to propose the idea of using a pattern language to architect buildings and cities.

What is the Singleton pattern?

I have made a video on this topic, which you can find here.

There are 23 software design patterns, which fall under three categories.

  1. Creational Patterns
  2. Structural Patterns
  3. Behavioral Patterns

The Singleton pattern falls under the Creational Patterns category, and its intent is the following.

Ensure a class only has one instance and provide a global point of access to it.

It should be noted that using the Singleton pattern in combination with other patterns is typical.

The elephant in the room 🐘🏠

If you have made it this far, you are either genuinely interested in learning how to use Singleton in test automation or are laughing hysterically, saying, “why in the world would you want to use Singleton…don’t you know it’s an anti-pattern?!”.

You will hear people say the following things about Singleton:
  • It violates the Dependency Inversion Principle.
  • It violates the Single Responsibility Principle.
  • It cannot be used with multi-threading.
  • It is tightly coupled.
  • It is difficult to test.
    etc.

I am well aware of the so-called disadvantages of using Singleton. However, many of these claims are untrue, and not all relate to software test automation.

It is my view that Singleton can prove useful in test automation framework design as long as it is implemented properly.

Generally speaking, I do not like “following the herd”, and when I hear people say that Singleton should be avoided at all costs, I want to know why.

You should do the same, and you may be surprised at what you find.

Now, with some Java examples, let us see what this pattern is all about ⬇️

How to use the Singleton pattern in Java

Let us consider the following hypothetical Java class called Controller.

public class Controller {}

We could then have another class called Demo to create multiple instances of Controller.

class Demo {
    public static void main(String[] args) {
        Controller c1 = new Controller();
        Controller c2 = new Controller();
        Controller c3 = new Controller();

        System.out.println(c1);
        System.out.println(c2);
        System.out.println(c3);
    }
}

Any time we print an object as we are doing above, we see an output of the form

package-name.class-name@hashcode

Hence, we get the following output when the program runs.

io.automatenow.Controller@58372a00
io.automatenow.Controller@4dd8dc3
io.automatenow.Controller@6d03e736

Notice that the hashcode is unique for each of the objects. That means that the Controller class is not using the Singleton pattern because we have distinct instances of the same object.

We must do three things to apply the Singleton pattern in our Controller class.

  1. Create a private static instance of Controller
  2. Create a private constructor
  3. Create a public method to access the Controller object

Our updated Controller class will be as follows:

public class Controller {
    private static Controller controller;

    private Controller(){}

    public static Controller getInstance() {
        if (controller == null) {
            controller = new Controller();
        }
        return controller;
    }
}

As a result, we get an error whenever we try to create a new instance of Controller in the Demo class.

class Demo {
    public static void main(String[] args) {
        Controller c1 = new Controller(); ← not allowed!
        Controller c2 = new Controller(); ← not allowed!
        Controller c3 = new Controller(); ← not allowed!

        System.out.println(c1);
        System.out.println(c2);
        System.out.println(c3);
    }
}

The error is as follows.

Controller() has private access in io.automatenow.Controller

Since we are now using the Singleton pattern, Java no longer allows us to create a new instance of Controller.

So, how can we access the Controller object?

We must use the getInstance() method that we created earlier.

Note:

You are not required to call this method getInstance(); it may very well be called instance() or get(). However, it is typical to see this name being used.

Let us update the Demo class. Instead of creating new instances of Controller, we will use the getInstance() method as follows.

class Demo {
    public static void main(String[] args) {
        Controller c1 = Controller.getInstance();
        Controller c2 = Controller.getInstance();
        Controller c3 = Controller.getInstance();

        System.out.println(c1);
        System.out.println(c2);
        System.out.println(c3);
    }
}

The output will resemble the following.

io.automatenow.Controller@4dd8dc3
io.automatenow.Controller@4dd8dc3
io.automatenow.Controller@4dd8dc3

Pay close attention to the class hashcode of each of the objects. Since all hashcodes are identical, we are dealing with a unique instance of Controller–thanks to using Singleton.

Great, but how can Singleton be applied in test automation?

How to use the Singleton pattern in test automation

There are several use cases for the Singleton pattern in test automation.

For instance, we could use it to load properties files, test data, or manage database connections.

The example we will use here is how to manage the driver in the popular test automation framework, Selenium WebDriver.

At first glance, it may seem counterintuitive to use the Singleton pattern to manage the driver in Selenium since it is highly likely that we would want to run our tests in parallel at some point. As you know, that is not possible when we only have one instance of WebDriver.

Not all is lost, however. Java offers us a solution, which is to use the ThreadLocal construct.

The Java documentation defines ThreadLocal as follows.

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable.

Based on that definition, we can conclude that each thread will have its own copy of the driver, making parallel testing possible. Here is how we can use the Singleton pattern to manage the driver instance.

Note:

Please click here to see the repo for the code you are about to see. You are welcome to clone the repo to try the code for yourself!

public class Driver {

    // ThreadLocal used to manage the driver
    private static ThreadLocal<WebDriver> driver = new ThreadLocal<>();

    // Private constructor to prevent the creation of new instances of Driver
    private Driver(){}

    // Public method to access the driver instance (uses lazy instantiation)
    public static WebDriver getInstance() {
        if (driver.get() == null) {
            WebDriverManager.chromedriver().setup();
            driver.set(new ChromeDriver());
        }

        return driver.get();
    }

/*
Public method to quit the driver and 
remove the current thread's value for this thread-local variable
*/
    public static void quit() {
        driver.get().quit();
        driver.remove();
    }
}

An example test class would look like the following. We have left the teardown() method within the test class for brevity, but this would generally go in a separate base test class.

package io.automatenow;

import org.testng.annotations.*;

public class TestBrowserNav {

    @AfterMethod
    void teardown() {
        Driver.quit();
    }

    @Test
    void test1() {
        Driver.getInstance().get("https://www.automatenow.io");
    }

    @Test
    void test2() {
        Driver.getInstance().get("https://www.selenium.dev");
    }

    @Test
    void test3() {
        Driver.getInstance().get("https://www.oracle.com");
    }
}

We can run all tests in parallel using a testng.xml file like the following.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd">
<suite name="Parallel Test Suite" parallel="methods">
    <test name="Browser Nav">
        <classes>
            <class name="io.automatenow.TestBrowserNav"/>
        </classes>
    </test>
</suite>

When we run the test suite above, we will see that the tests run in parallel without any issues.

Mission accomplished 🎉

Benefits of using Singleton

Some of the advantages of using Singleton are listed below.

  • Access control: the Singleton class controls how and when clients can use its single instance. It does this by using encapsulation.
  • Better memory management: the Singleton pattern prohibits the creation of new class instances, which reduces memory requirements.
  • Allows more than one instance: it is easy for us to change the default behavior of having only one Singleton class instance and allow more than one.
  • Superior over class operations: although it is possible to mimic Singleton functionality through class operations (e.g., using static methods in Java), allowing more than one class instance is difficult.

Conclusion

The Singleton pattern is an excellent addition to our testing toolset! It comes in handy when designing a test automation framework in which we need to control access to certain things.

In the case of Selenium WebDriver, it is common to have n number of tests; however, there should always only be one instance of WebDriver. The Singleton pattern is a great way to ascertain the same.

References

Gamma, Helm, Johnson, Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley, 1995.

Accelerate App Testing
With centralized web-based test automation Agile, DevOps and CI\CD management solution ⚙️
Follow us