logo logo

Design Patterns for High-Quality Automated Tests: Singleton

Design Patterns for High-Quality Automated Tests: Singleton

In the previous article from the series we discussed the Fluent Interface design pattern and achieve maximum API usability. It is implemented through method chaining (method cascading). In the current publication, I am going to share with you how to create only once your page object models and afterwards reuse them. To do that we are going to use the Singleton design pattern.

Test Case

We will automate the main Bing search main page. All of the code is placed inside the BingMainPage class.

Design Patterns for High-Quality Automated Tests: Singleton

Page Object Model – Non-Singleton Version

Let us see how a regular non-singleton page object looks and how we use it in tests. Then we are going to refactor it and make it a singleton.

public class BingMainPage
{
    private readonly IWebDriver _driver;
    private readonly string _url = @"http://www.bing.com/";

    public BingMainPage(IWebDriver browser)
    {
        _driver = browser;
    }

    public void Navigate()
    {
        _driver.Navigate().GoToUrl(_url);
    }

    public void Search(string textToType)
    {
        GetSearchBox().Clear();
        GetSearchBox().SendKeys(textToType);
        GetGoButton().Click();
    }

    public void AssertResultsCount(string expectedCount)
    {
        Assert.AreEqual(GetResultsCountDiv().Text, expectedCount);
    }

    private IWebElement GetSearchBox()
    {
        return _driver.FindElement(By.Id("sb_form_q"));
    }

    private IWebElement GetGoButton()
    {
        return _driver.FindElement(By.Id("sb_form_go"));
    }

    private IWebElement GetResultsCountDiv()
    {
        return _driver.FindElement(By.Id("b_tween"));
    }
}

Note: I put the assertion methods here as well. It is suggested to add them if you are going to use them in more than one test, especially if you have set a detailed exception method.

Also, to make the example simpler, I haven’t created a separate class for the element maps, as we did in the Fluent API article.

Non-Singleton Version in Tests

The tests are implemented using the MSTest framework. Before all tests, we start the browser, reuse it for all, and at the end of the run, we close it. We need to create a new instance of the page object model before using it. If we use the page in all tests in the file, we can create a single shared instance in a TestInitialize method.

[TestClass]
public class BingTests
{
    private IWebDriver _driver;

    [TestInitialize]
    public void TestInitialize()
    {
        _driver = new FirefoxDriver();
        _driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(30);
    }

    [TestCleanup]
    public void TestCleanup()
    {
        _driver.Quit();
    }

    [TestMethod]
    public void SearchTextInBing_First()
    {
        var bingMainPage = new BingMainPage(_driver);

        bingMainPage.Navigate();
        bingMainPage.Search("Automate The Planet");

        bingMainPage.AssertResultsCount("236,000 RESULTS");
    }
}

The drawback of this approach is that we need every time to initialize our pages. Here the constructor of the page takes only a single parameter, but sometimes there are more parameters. If we use this kind of initialization in many tests, later test changes will be more challenging and more error prompt. Moreover, since the pages are stateless, we do not need to initialize them multiple times since the elements are always located in the current browser instance.

Now, we will discuss an alternative approach using the Singleton design pattern.

Singleton Design Pattern

Definition:

The class has only one instance and provides access to it.

UML Class Diagram:

singleton-design-pattern-uml-class-diagram

Participants:

The classes and objects participating in this pattern are:

  • Base Page – holds a static property to the instance of the class. It is created on the first call to the class. The class cannot be created through the usage of the keyword new.
  • Concrete Page – inherits the base page. Can be used in tests through the Instance property but cannot be created through the usage of the keyword new.

The basic implementation of the pattern requires a static variable and a static property. Also, to make sure that the class cannot be initialized through the keyword new, the constructor access modifier is set to private.

Page Object Model – Singleton Version

public class BingMainPage : WebPage<BingMainPage>
{
    private readonly string _url = @"http://www.bing.com/";

    public void Navigate()
    {
        WrappedDriver.Navigate().GoToUrl(_url);
    }

    public void Search(string textToType)
    {
        GetSearchBox().Clear();
        GetSearchBox().SendKeys(textToType);
        GetGoButton().Click();
    }

    public void AssertResultsCount(string expectedCount)
    {
        Assert.AreEqual(GetResultsCountDiv().Text, expectedCount);
    }

    private IWebElement GetSearchBox()
    {
        return WrappedDriver.FindElement(By.Id("sb_form_q"));
    }

    private IWebElement GetGoButton()
    {
        return WrappedDriver.FindElement(By.Id("sb_form_go"));
    }

    private IWebElement GetResultsCountDiv()
    {
        return WrappedDriver.FindElement(By.Id("b_tween"));
    }
}

Driver

Below you can find a static utility class that we use to create a single instance of WebDriver. It comes in handy instead of copy-paste the same code repeatedly in the TestInitialize methods.

public static class Driver
{
    private static WebDriverWait _browserWait;

    private static IWebDriver _browser;

    public static IWebDriver GetBrowser()
    {
        if (_browser == null)
        {
            throw new NullReferenceException("The WebDriver browser instance was not initialized. You should first call the method Start.");
        }
        return _browser;
    }

    private static void SetBrowser(IWebDriver value)
    {
        _browser = value;
    }

    public static WebDriverWait GetBrowserWait()
    {
        if (_browserWait == null || _browser == null)
        {
            throw new NullReferenceException("The WebDriver browser wait instance was not initialized. You should first call the method Start.");
        }
        return _browserWait;
    }

    private static void SetBrowserWait(WebDriverWait value)
    {
        _browserWait = value;
    }

    public static void StartBrowser(BrowserType browserType = BrowserType.Firefox, int defaultTimeOut = 30)
    {
        switch (browserType)
        {
            case BrowserType.Firefox:
                SetBrowser(new FirefoxDriver());
                break;
            case BrowserType.InternetExplorer:
                break;
            case BrowserType.Chrome:
                break;
            default:
                throw new ArgumentException("You need to set a valid browser type.");
        }

        SetBrowserWait(new WebDriverWait(GetBrowser(), TimeSpan.FromSeconds(defaultTimeOut)));
    }

    public static void StopBrowser()
    {
        GetBrowser().Quit();
        SetBrowser(null);
        SetBrowserWait(null);
    }
}

WebPage

We hold here a static instance of our page. We create it only once. Afterward, each time we request the instance, we return the already created value. The built-in .NET framework generic class Lazy<T> saves some code for implementing the lazy initialization. Also, it is thread-safe.

Note: Lazy initialization is a technique that defers the creation of an object until the first time it is needed. In other words, the initialization of the object happens only on demand. Note that the terms lazy initialization and lazy instantiation mean the same thing – they can be used interchangeably.

To follow the singleton design pattern more closely, we can add a private constructor to the page, and instead of using the new keyword to create the instance, we can use Reflection.

public abstract class WebPage<TPage>
    where TPage : new()
{
    private static readonly Lazy<TPage> _lazyPage 
        = new Lazy<TPage>(() => new TPage());

    protected readonly IWebDriver WrappedDriver;

    protected WebPage()
    {
        WrappedDriver = Driver.GetBrowser() ?? throw new ArgumentNullException("The wrapped IWebDriver instance is not initialized.");
    }

    public static TPage GetInstance()
    {
        return _lazyPage.Value;
    }
}

Note: Generic classes allow you to define code that can be used as a template instead of specifying separate classes for each type. You provide the type as a parameter in the class definition. The generics were added to version 2.0 of C#. There are similar implementations for Java, C++ and other major programming languages.

Singleton Version in Tests

[TestClass]
public class BingTests
{
    private IWebDriver _driver;

    [TestInitialize]
    public void TestInitialize()
    {
        Driver.StartBrowser();
    }

    [TestCleanup]
    public void TestCleanup()
    {
        Driver.StopBrowser();
    }

    [TestMethod]
    public void Singleton_SearchTextInBing_First()
    {
        BingMainPage.GetInstance().Navigate();
        BingMainPage.GetInstance().Search("Automate The Planet");

        BingMainPage.GetInstance().AssertResultsCount("236,000 RESULTS");
    }
}

Now we do not have the WebDriver initialization logic in the TestInitialize method. Also, the usage of the page object in the test is changed. We use the Instance static getter method to retrieve the instance of the page.

Summary

As we mentioned above, the API is the specification of what you can do with a software library. When we use the term usability together with the term API, it means “How easy it is for you as a user to find out what the methods do and how to use them?”. In the case of a Test Library – “How much time a new user needs to create a new test?“.

In the programming community, we sometimes use another term for the same thing called syntactic sugar. It describes how easy it is to use or read some expressions. It sweetens the programming languages for humans 🍬 The programming statements become more concise and clearer. Singleton design pattern can be applied in automated testing to improve API usability and help us write tests faster with less code. 

In the next articles from the series, we will look into other essential design patterns that can help you write more maintainable, readable, and stable automated tests.

For more detailed overview and usage of many more design patterns and best practices in automated testing, check my book “Design Patterns for High-Quality Automated Tests, C# Edition, High-Quality Tests Attributes, and Best Practices“.  You can read part of three of the chapters:

Happy Testing! 🌠

Anton S. Angelov

About the author

Anton S. Angelov

CTO and Co-founder of Automate The Planet Ltd, inventor of BELLATRIX Test Automation Framework and MEISSA Distributed Test Runner. I have more than ten years in the field of automated testing. For more than six years I worked as QA architect in two big companies- Telerik (back then the biggest software company in Bulgaria) and later in US company called Progress (similar in size). Part of my job was to design and write scalable test automation framework that more than ten teams had to use. In parallel, I consulted a couple of companies regarding test automation and led several related pieces of training. I was nominated four times for best QA in Bulgaria 2017, 2018 (won), 2019 and 2020.

Author of the book- “Design Patterns for High-Quality Automated Tests, C# Edition, High-Quality Tests Attributes, and Best Practices” (#1 New Release, Best Seller under Quality Control on Amazon)

I am an international conference speaker spoke at events (such as Selenium Conf, Appium Conf, Heisenbug) in Russia, India, Romania, Serbia, Netherlands, Poland, Ukraine and many more. Won a couple of times best paper award. Code Project MVP for 2016-2018 and DZone most valuable blogger with more than 150 articles with over 3 million views. I am writing technical articles each week for Automate The Planet Blog for past five years (250+). Last year the website was mentioned five times as one of the top 10 testing blogs in the world. Articles of mine were published in the previous five editions of the Quality Matters Testing Magazine.

– 280+ Published Articles Automate The Planet
– 120+ Published Articles Code Project
– 60+ Published Articles DZone as Most Valuable Blogger
– 6+ Articles Published in Quality Magazines
– 20+ Given International Conferences Talks
– 2 books published
– 5,000,000+ article views
– 1000 000+ amazing readers for 2020
– Read in 180+ countries

Join TestProject Community

Get full access to the world's first cloud-based, open source friendly testing community. Enjoy TestProject's end-to-end test automation Platform, Forum, Blog and Docs - All for FREE.

Join Us Now  

Comments

14 1 comment
  • Avatar
    Sim Katyal March 16, 2021, 7:22 am

    Isn’t using “private static IWebDriver _browser;” thread-unsafe so if one thread kills the browser, it impacts the test running in other thread?

Leave a Reply

popup image

Become a master for Selenium testing

Getting started with web testing? We’ve got you covered with a FREE ready-to-go test automation platform that’s already bundled up with Selenium to simplifying and enhancing your experience.
Get Started
FacebookLinkedInTwitterEmail