Beginning a Journey With Selenium WebDriver and C#

Enes Kuhn

In the following tutorial I m going to show you how to create your own Selenium WebDriver C# automation framework with the help of four design patterns:

  • Page Object pattern
  • Facade pattern
  • Singleton pattern
  • Null object pattern

Prerequisites:

Note: Check .NET desktop development workload during installation

Create a new unit testing project

Install and open Microsoft Visual Studio.
From the main window select File > New > Project

Create a new project

From the left-hand pane select Visual C# > Test, select Unit Test Project, type in project details: Name, Location and Framework and click on the OK button.

Select Unit Test Project

If you bring up the Solution Explorer window, you will notice that a new project is created with the project structure shown as in the picture below.

The SeleniumProject

In order to proceed with framework creation, we need to include several NuGet packages to the solution. Right click on the References and select Manage NuGet Packages (NuGet is a free and open-source package manager designed for the Microsoft development platform. Read more here).

Open Manage NuGet Package Manager

Install the following packages one by one:

  • NUinit
  • NUnit3TestAdapter
  • Selenium.Chrome.WebDriver
  • Selenium.Firefox.WebDriver
  • Selenium.Support
  • Selenium.WebDriver
  • BasePageObjectModel.NUnit
  • DotNetSeleniumExtras.PageObjects
  • DotNetSeleniumExtras.WaitHelpers

After you complete the installation process your References three should look like this:

The SeleniumProject structure

For the demo purpose, I m going to use a dummy nopCommerce site: https://demo.nopcommerce.com/.

Under the SeleniumProject solution, create following folder structure Assembly Pages3 Test

The SeleniumProject structure

Page Object Model design pattern

POM is the most widely used design pattern in the Selenium community in which each web page (or at least the significant ones) is considered as a different class.

On each of these page classes (i.e page objects), you may define its elements and specific methods/actions. Each page class represents the page of the web application or its fragment. It is a layer between the test scripts and UI and encapsulates the features of the page.

Let s create a Browsers class that is going to be responsible for Selenium WebDriver instance. Right-click on the Assembly folder and select Add > Class. Name it “Browsers.cs” and click on the Add button.

Add the following code to the class and save it:

using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
using OpenQA.Selenium.Firefox;
namespace SeleniumProject
{
    public class Browsers
    {
        private static IWebDriver webDriver;
        private static string baseURL = "https://demo.nopcommerce.com/";
        private static string browser = "Chrome";
        public static void Init()
        {
            switch (browser)
            {
                case "Chrome":
                    webDriver = new ChromeDriver();
                    break;
                case "Firefox":
                    webDriver = new FirefoxDriver();
                    break;
                default:
                    webDriver = new ChromeDriver();
                    break;
            }
            webDriver.Manage().Window.Maximize();
            Goto(baseURL);
        }
        public static string Title
        {
            get { return webDriver.Title; }
        }
        public static IWebDriver getDriver
        {
            get { return webDriver; }
        }
        public static void Goto(string url)
        {
            webDriver.Url = url;
        }
        public static void Close()
        {
            webDriver.Quit();
        }
    }
}
Browsers.cs

Create a new class under the Pages folder (Right click at Pages folder > Add > Class). Name it “Home.cs” and click on the Add button.

Add Home page class (map) to the framework

Enter locators and actions:

using NUnit.Framework;
using OpenQA.Selenium;
using SeleniumExtras.PageObjects;
namespace SeleniumProject
{
    public class Home
    {
        //Locators
        [FindsBy(How = How.CssSelector, Using = "#small-searchterms")]
        private IWebElement SearchStoreInput;
        [FindsBy(How = How.XPath, Using = "//input[@value='Search']")]
        private IWebElement SearchButton;
        //Actions
        public void isAt()
        {
            Assert.IsTrue(Browsers.Title.Equals("nopCommerce demo store"));
        }
        public void EnterSearchText(string searchText)
        {
            Assert.IsTrue(SearchStoreInput.Displayed);
            SearchStoreInput.SendKeys(searchText);
        }
    }
}
Home.cs

For more details about Selenium locators and actions read here and here.

I recommend ChroPath browser extension for fetching locators quickly and efficiently.

Create a new class Page, under the Assembly folder that will handle the page maps.

using SeleniumExtras.PageObjects;
namespace SeleniumProject
{
    public static class Pages
    {
        private static T getPages<T>() where T : new()
        {
            var page = new T();
            PageFactory.InitElements(Browsers.getDriver, page);
            return page;
        }
        public static Home home
        {
            get { return getPages<Home>(); }
        }
    }
}
Pages.cs

As of now, our project should have structure as shown below:

The SeleniumProject structure

Create the first “Hello World” test

Drag and drop UnitTest1.cs from the root of the project to the Tests folder, rename it to HelloWorldTest.cs and update it:

using NUnit.Framework;
using SeleniumProject;
namespace Tests
{
    [TestFixture]
    public class MyFirstPOMTest
    {
        [SetUp]
        public void StartUpTest()
        {
            Browsers.Init();
        }
        [TearDown]
        public void EndTest()
        {
            Browsers.Close();
        }
        [Test]
        public void HelloWorldTest()
        {
            Pages.home.isAt();
            Pages.home.EnterSearchText("Hello World");
        }
    }
}
HelloWorldTest.cs

Bring up the Test Explorer window by clicking on the Test > Windows > Test Explorer from the top menu of the Visual Studio, build the solution and run the test by right-clicking on it at the Test Explorer and selecting Run Selected Test.

Test passed

In order to add more pages to the solution. Add a new class in the Pages folder and register it in Pades.cs class the same way we did it for Home.cs

Facade design pattern

The facade pattern is a software-design pattern commonly used in object-oriented programming. Analogous to a facade in architecture, a facade is an object that serves as a front-facing interface masking more complex underlying or structural code.

We are going to make a custom facade around WebDriver API in order to add more power to the WebDriver methods by adding explicit waits and log.

Let s rename the existing Browsers.cs to WebDriverFacade.cs and add more cool stuff. The new class should look like this:

using NUnit.Framework;
using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
using OpenQA.Selenium.Firefox;
using OpenQA.Selenium.Interactions;
using OpenQA.Selenium.Support.UI;
using System;
namespace SeleniumProject
{
    public static class WebDriverFacade
    {
        private static IWebDriver webDriver;
        private static string baseURL = "https://demo.nopcommerce.com/";
        private static string browser = "Chrome";
        public static void Init()
        {
            switch (browser)
            {
                case "Chrome":
                    webDriver = new ChromeDriver();
                    break;
                case "Firefox":
                    webDriver = new FirefoxDriver();
                    break;
                default:
                    webDriver = new ChromeDriver();
                    break;
            }
            webDriver.Manage().Window.Maximize();
            Console.WriteLine(string.Format("[{0}] - Web browser started", DateTime.Now.ToString("HH:mm:ss.fff")));
            Goto(baseURL);
            Console.WriteLine(string.Format("[{0}] - Url [{1}] initiated", DateTime.Now.ToString("HH:mm:ss.fff"), baseURL));
        }
        public static string Title
        {
            get { return webDriver.Title; }
        }
        public static IWebDriver getDriver
        {
            get { return webDriver; }
        }
        public static void Goto(string url)
        {
            webDriver.Url = url;
        }
        public static void Close()
        {
            webDriver.Quit();
        }
        //extensions
        public static bool ControlExists(this IWebDriver driver, By by)
        {
            return driver.FindElements(by).Count == 0 ? false : true;
        }
public static bool ControlDisplayed(this IWebElement element, bool displayed = true, uint timeoutInSeconds = 60)
        {
            var wait = new WebDriverWait(webDriver, TimeSpan.FromSeconds(timeoutInSeconds));
            wait.IgnoreExceptionTypes(typeof(Exception));
            return wait.Until(drv =>
            {
                if (!displayed && !element.Displayed || displayed && element.Displayed)
                {
                    return true;
                }
                return false;
            });
        }
public static IWebElement IsElementExists(this By Locator, uint timeoutInSeconds = 60)
        {
            try
            {
                WebDriverWait wait = new WebDriverWait(webDriver, TimeSpan.FromSeconds(timeoutInSeconds));
                return wait.Until(SeleniumExtras.WaitHelpers.ExpectedConditions.ElementExists(Locator));
            }
            catch
            {
                return null;
            }
        }
public static bool ElementlIsClickable(this IWebElement element, uint timeoutInSeconds = 60, bool displayed = true)
        {
            try
            {
                WebDriverWait wait = new WebDriverWait(webDriver, TimeSpan.FromSeconds(timeoutInSeconds));
                return wait.Until(drv =>
                {
                    if (SeleniumExtras.WaitHelpers.ExpectedConditions.ElementToBeClickable(element) != null)
                        return true;
return false;
                });
            }
            catch
            {
                return false;
            }
        }
public static void ClickWrapper(this IWebElement element, string elementName)
        {
            if (element.ElementlIsClickable())
            {
                element.Click();
            }
            else
            {
                throw new Exception(string.Format("[{0}] - Element [{1}] is not displayed", DateTime.Now.ToString("HH:mm:ss.fff"), elementName));
            }
        }
public static void SendKeysWrapper(this IWebElement element, string value, string elementName)
        {
            Console.WriteLine(string.Format("[{0}] - SendKeys value [{1}] to  element [{2}]", DateTime.Now.ToString("HH:mm:ss.fff"), value, elementName));
            element.SendKeys(value);
        }
public static void DoubleClickActionWrapper(this IWebElement element, string elementName)
        {
            Actions ClickButton = new Actions(webDriver);
            ClickButton.MoveToElement(element).DoubleClick().Build().Perform();
            Console.WriteLine("[{0}] - Double Click on element [{1}]", DateTime.Now.ToString("HH:mm:ss.fff"), elementName);
        }
public static void ClearWrapper(this IWebElement element, string elementName)
        {
            Console.WriteLine("[{0}] - Clear element [{1}] content", DateTime.Now.ToString("HH:mm:ss.fff"), elementName);
            element.Clear();
            Assert.AreEqual(element.Text, string.Empty, "Element is not cleared");
        }
public static void CheckboxWrapper(this IWebElement element, bool value, string elementName)
        {
            Console.WriteLine("[{0}] - Set value of checkbox [{1}] to [{2}]", DateTime.Now.ToString("HH:mm:ss.fff"), elementName, value.ToString());
if ((!element.Selected && value == true) || (element.Selected && value == false))
            {
                element.Click();
            }
        }
}
}
The updated Browsers.cs (WebDriverFacade.cs)

Now, update all the Browsers references to WebDriverFacade in HelloWorldTest.cs, Pages.cs and Home.cs.

Update existing EnterSearchText (Home.cs):

public void EnterSearchText(string searchText)
{
       Assert.IsTrue(SearchStoreInput.ControlDisplayed());
       SearchStoreInput.SendKeysWrapper(searchText, "Search input");
}

Rebuild the solution, run the test from Test Explorer and verify the new results by clicking on the Output link.

The new test output

In this section, we added more power to the existing Selenium WebDriver by adding explicit waits and log-to-console. Any other WebDriver method can be wrapped as well.

Singleton pattern

When we create a class that restricts the instantiation of a class to one “single” instance, it is called the Singleton design pattern. It is very useful when you need to use the same object of a class across all classes or framework. Singleton class returns the same instance if it is instantiated again.

Add the following code to the WebDriverFacade.cs class below the webDriver property definition:

public static IWebDriver WebDriver
{
 get
 {
  if (webDriver == null)
  {
   webDriver = new ChromeDriver();
  }
  return webDriver;
 }
}
Updated WebDriverFacade.cs class

Null object pattern

In object-oriented computer programming, a Null Object is an object with no referenced value or with defined neutral (“null”) behavior. The Null Object Design Pattern describes the uses of such objects and their behavior (or lack thereof).

For our automation testing purpose, we can create a new class NullWebElement.cs in the Assembly folder as shown below:

using OpenQA.Selenium;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Drawing;
public class NullWebElement : IWebElement
{
    private const string nullWebElement = "NullWebElement";
    public string TagName { get { return nullWebElement; } }
    public string Text { get { return nullWebElement; } }
    public bool Enabled { get { return false; } }
    public bool Selected { get { return false; } }
    public Point Location { get { return new Point(0, 0); } }
    public Size Size { get { return new Size(0, 0); } }
    public bool Displayed { get { return false; } }
    public void Clear() { }
    public void Click() { }
    public string GetAttribute(string attributeName) { return nullWebElement; }
    public string GetCssValue(string propertyName) { return nullWebElement; }
    public string GetProperty(string propertyName) { return nullWebElement; }
    public void SendKeys(string text) { }
    public void Submit() { }
public IWebElement FindElement(By by) { return this; }
    public ReadOnlyCollection<IWebElement> FindElements(By by)
    {
        return new ReadOnlyCollection<IWebElement>(new List<IWebElement>());
    }
private NullWebElement() { }
private static NullWebElement instance;
    public static NullWebElement NULL
    {
        get
        {
            if (instance == null)
            {
                instance = new NullWebElement();
            }
            return instance;
        }
    }
}

There are two major benefits of NullWebElement class.

  • when we have an optional element on the page (do not fail test if missing)
try
{
    element.SendKeys(“Test”);
}
catch
{
    element = NullWebElement.NULL;
}
  • when checking if the element is found
if (element == NullWebElement.NULL)
{
    Console.WriteLine("Element not found!");

Run tests outside of MS Visual Studio (scheduled test run)

I m sure you ll agree that tests aren t fully automated unless we make them run without end-user interaction. Our goal is to have tests running outside of our working hours or after the application code deployment.

In order to run the tests outside of visual studio use the VSTest.Console application (command-line tool to run tests).

Search the VS installation folder for “VSTest.console.exe”. On my machine it s located in “C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\Common7\IDE\CommonExtensions\Microsoft\TestWindow\Vstest.console.exe”

Now, search the SeleniumProject folder for “SeleniumProject.dll”. On my machine it s in “C:\SeleniumProject\SeleniumProject\SeleniumProject\bin\Debug\SeleniumProject.dll”

You won t believe me, but, in order to run the tests outisde of MS Visual Studio you just need to type in those two locations in CMD, press Enter and that s it.

Note: Make sure to leave quotation marks

"C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\Common7\IDE\CommonExtensions\Microsoft\TestWindow\Vstest.console.exe" "C:\SeleniumProject\SeleniumProject\SeleniumProject\bin\Debug\SeleniumProject.dll"
CMD command

How can you know if your test fails? Good question.

The VSTest.Console has a mechanism to tell you that.
Add following postfix to the command: /Tests:HelloWorldTest /Logger:trx;LogFileName=C:\Output\Resut.trx

  • /Tests:HelloWorldTest explicitly says which test(s) to run, othervise it will run all the tests from test class
  • /Logger:trx;LogFileName=C:\Output\Resut.trx it creates a new output folder and the file

The new command will now look like this:

"C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\Common7\IDE\CommonExtensions\Microsoft\TestWindow\Vstest.console.exe" "C:\SeleniumProject\SeleniumProject\SeleniumProject\bin\Debug\SeleniumProject.dll" /Tests:HelloWorldTest /Logger:trx;LogFileName=C:\Output\Resut.trx

Run it and examine the Results.trx file!

If you like to run your tests at a certain time – Windows Task Scheduler is your friend.

I hope you find this tutorial useful. For the next step, try to make your tests data driven! If you have any questions or suggestions feel free to contact me at enesku@maestralsolutions.com

Leave a Reply

Your email address will not be published. Required fields are marked *

After you leave a comment, it will be held for moderation, and published afterwards.


The reCAPTCHA verification period has expired. Please reload the page.