Mistral logo

Beginning a Journey With Selenium WebDriver and C Sharp

27.06.2019 - READING TIME: 10 minutes

Beginning a Journey With Selenium WebDriver and C Sharp

Author: 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
  • Façade 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 — Pages — 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() where T : new()
{
var page = new T();
PageFactory.InitElements(Browsers.getDriver, page);
return page;
}
public static Home home
{
get { return getPages(); }
}
}
}

 

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


Façade design pattern

The façade 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 façade 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 FindElements(By by)
{
return new ReadOnlyCollection(new List());
}
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.Consoleapplication (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 leave a comment below.