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.
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:
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:
- Defining High-Quality Test Attributes for Automated Tests
- Benchmarking for Assessing Automated Test Components Performance
- Generic Repository Design Pattern- Test Data Preparation
Happy Testing! 🌠
Isn’t using “private static IWebDriver _browser;” thread-unsafe so if one thread kills the browser, it impacts the test running in other thread?