Enhanced Selenium WebDriver Tests with the New Improved C# 6.0

Selenium WebDriver C# 6.0

I believe that the research is one of the best ways to improve your existing processes. Recently I read a couple of articles about the new cool features of the C# language. There are tonnes of them. However, I am not sure that most people realize how to apply them to make their tests even better. This article will be about that- I will show you how different Selenium WebDriver tests look using C# 5.0 and how they can be prettified using the new improved C# 6.0. You can find even more attractive ideas about your Selenium WebDriver tests in my WebDriver Series. I will create a similar article dedicated to the not released yet C# 7.0.

1. Expression Bodied Function & Property

C# 5.0 Version- Expression Bodied Functions

Below you can find a page object that represents the SearchEngine main search page. It contains the elements of the page, a search method and a single assertion that verifies the number of the returned results. There are two methods that contain only a single line of code- Navigate and AssertResultsCount. However, each one of them spans over four lines.

public class SearchEngineMainPage
{
    private readonly IWebDriver driver;
    public SearchEngineMainPage(IWebDriver browser)
    {
        this.driver = browser;
        PageFactory.InitElements(browser, this);
    }
    public string Url
    {
        get
        {
            return @"searchEngineUrl";
        }
    }
    [FindsBy(How = How.Id, Using = "sb_form_q")]
    public IWebElement SearchBox { get; set; }
    [FindsBy(How = How.Id, Using = "sb_form_go")]
    public IWebElement GoButton { get; set; }
    [FindsBy(How = How.Id, Using = "b_tween")]
    public IWebElement ResultsCountDiv { get; set; }
    public void Navigate()
    {
        this.driver.Navigate().GoToUrl(this.Url);
    }
    public void Search(string textToType)
    {
        this.SearchBox.Clear();
        this.SearchBox.SendKeys(textToType);
        this.GoButton.Click();
    }
    public void AssertResultsCount(string expectedCount)
    {
        Assert.AreEqual(this.ResultsCountDiv.Text, expectedCount);
    }
}

C# 6.0 Version- Expression Bodied Functions

With the new C# 6.0 syntax, these methods take only a single line of code. How cool is that?!

public class SearchEngineMainPage
{
    private readonly IWebDriver driver;
    private readonly string url = @"searchEngineUrl";
    public SearchEngineMainPage(IWebDriver browser)
    {
        this.driver = browser;
        PageFactory.InitElements(browser, this);
    }
    public string Url => @"searchEngineUrl";
    [FindsBy(How = How.Id, Using = "sb_form_q")]
    public IWebElement SearchBox { get; set; }
    [FindsBy(How = How.Id, Using = "sb_form_go")]
    public IWebElement GoButton { get; set; }
    [FindsBy(How = How.Id, Using = "b_tween")]
    public IWebElement ResultsCountDiv { get; set; }
    public void Navigate() => this.driver.Navigate().GoToUrl(this.url);
    public void Search(string textToType)
    {
        this.SearchBox.Clear();
        this.SearchBox.SendKeys(textToType);
        this.GoButton.Click();
    }
    public void AssertResultsCount(string expectedCount) => Assert.AreEqual(this.ResultsCountDiv.Text, expectedCount);
}

C# 5.0 Version- Expression Bodied Properties

SearchEngineMainPage.Map

This is another way of getting the elements of a web page. However, every element spans over seven lines of code!

public partial class SearchEngineMainPage
{
    public IWebElement SearchBox
    {
        get
        {
            return this.driver.FindElement(By.Id("sb_form_q"));
        }
    }
    public IWebElement GoButton
    {
        get
        {
            return this.driver.FindElement(By.Id("sb_form_go"));
        }
    }
    public IWebElement ResultsCountDiv
    {
        get
        {
            return this.driver.FindElement(By.Id("b_tween"));
        }
    }
}

C# 6.0 Version- Expression Bodied Properties

SearchEngineMainPage.Map

Here, we can use again the new improved C# 6.0 to rewrite the properties, now each one of them takes only a single line of code. This makes our element map class much shorter.

public partial class SearchEngineMainPage
{
    public IWebElement SearchBox => this.driver.FindElement(By.Id("sb_form_q"));
    public IWebElement GoButton => this.driver.FindElement(By.Id("sb_form_go"));
    public IWebElement ResultsCountDiv => this.driver.FindElement(By.Id("b_tween"));
}

SearchEngineMainPage

The same can be used for properties of the page such as the URL.

public partial class SearchEngineMainPage
{
    private readonly IWebDriver driver;
    public SearchEngineMainPage(IWebDriver browser)
    {
        this.driver = browser;
        PageFactory.InitElements(browser, this);
    }
    public string Url => @"searchEngineUrl";
    public void Navigate() => this.driver.Navigate().GoToUrl(this.Url);
    public void Search(string textToType)
    {
        this.SearchBox.Clear();
        this.SearchBox.SendKeys(textToType);
        this.GoButton.Click();
    }
}

2. Auto-Property Initializers

C# 5.0 Version

Sometimes, we use an object to pass more data to our tests. Most of the times these objects have default values. If we don't specify the values through the first constructor, we use the default one where the default values are initialized.

public class Client
{
    public Client(string firstName, string lastName, string email, string password)
    {
        this.FirstName = firstName;
        this.LastName = lastName;
        this.Email = email;
        this.Password = password;
    }
    public Client()
    {
        this.FirstName = "Default First Name";
        this.LastName = "Default Last Name";
        this.Email = "myDefaultClientEmail@gmail.com";
        this.Password = "12345";
    }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Email { get; set; }
    public string Password { get; set; }
}

C# 6.0 Version

With the new C# 6.0 auto-property initializers, we don't need the default constructor anymore. You can assign the default values directly after the properties. In my opinion, this syntax is much more readable.

public class Client
{
    public Client(string firstName, string lastName, string email, string password)
    {
        this.FirstName = firstName;
        this.LastName = lastName;
        this.Email = email;
        this.Password = password;
    }
    public string FirstName { get; set; } = "Default First Name";
    public string LastName { get; set; } = "Default Last Name";
    public string Email { get; set; } = "myDefaultClientEmail@gmail.com";
    public string Password { get; set; } = "12345";
}

3. nameOf Expression

C# 5.0 Version

If we want to bulletproof our test framework's API, we usually add validations and throw exceptions. A common practice is to specify the name of the field that was not initialized properly. The problem with the code below is that if you rename the field, it won't be changed in the exception's message. 

public void Login(string email, string password)
{
    if (string.IsNullOrEmpty(email))
    {
        throw new ArgumentException("Email cannot be null or empty.");
    }
    if (string.IsNullOrEmpty(password))
    {
        throw new ArgumentException("Password cannot be null or empty.");
    }
    // login the user
}

C# 6.0 Version

With the new nameof operator, you can get the name of the field. If it is renamed, the message will be changed too.

public void Login(string email, string password)
{
    if (string.IsNullOrEmpty(email))
    {
        throw new ArgumentException(nameof(email) + " cannot be null or empty.");
    }
    if (string.IsNullOrEmpty(password))
    {
        throw new ArgumentException(nameof(password) + " cannot be null or empty.");
    }
    // login the user
}

4. Null Conditional Operator

C# 5.0 Version

When we need to automate more complex cases such as filling the billing or shipping information for creating an online purchase. Imagine a site like Online Store. We don't want to pass 10 parameters to our methods instead we use custom objects such as the ClientPurchaseInfo that holds the whole info about the client's inputs. However, some of the fields are optional and if they are null, you don't need to type anything. This is valid for the Zip and the VAT ID. It is hard for me to "parse" a syntax as the one on lines- 20, 21, 22.

public partial class ShippingAddressPage
{
    private readonly IWebDriver driver;
    private readonly string url = @"onlineStoreUrlshippingPage";
    public ShippingAddressPage(IWebDriver browser)
    {
        this.driver = browser;
        PageFactory.InitElements(browser, this);
    }
    // some other actions
    private void FillAddressInfoInternal(ClientPurchaseInfo clientInfo)
    {
        this.Country.SelectByText(clientInfo.Country);
        this.FullName.SendKeys(clientInfo.FullName);
        this.Address.SendKeys(clientInfo.Address);
        this.City.SendKeys(clientInfo.City);
        this.Zip.SendKeys(clientInfo.Zip == null ? string.Empty : clientInfo.Zip);
        this.Phone.SendKeys(clientInfo.Phone == null ? string.Empty : clientInfo.Phone);
        this.Vat.SendKeys(clientInfo.Vat == null ? string.Empty : clientInfo.Vat);
    }
}

C# 6.0 Version

When you use the new null conditional operator (clientInfo?.Zip) and you access the VAT property if it is null, instead of throwing NullReferenceException, a null value will be returned. When we combine it with the ?? operator, and the value is null, the right value of the expression will be returned. I believe that the new C# 6.0 syntax is much more readable.

public partial class ShippingAddressPage
{
    private readonly IWebDriver driver;
    private readonly string url = @"onlineStoreUrlshippingPage";
    public ShippingAddressPage(IWebDriver browser)
    {
        this.driver = browser;
        PageFactory.InitElements(browser, this);
    }
    // some other actions
    private void FillAddressInfoInternal(ClientPurchaseInfo clientInfo)
    {
        this.Country.SelectByText(clientInfo.Country);
        this.FullName.SendKeys(clientInfo.FullName);
        this.Address.SendKeys(clientInfo.Address);
        this.City.SendKeys(clientInfo.City);
        this.Zip.SendKeys(clientInfo?.Zip ?? string.Empty);
        this.Phone.SendKeys(clientInfo?.Phone ?? string.Empty);
        this.Vat.SendKeys(clientInfo?.Vat ?? string.Empty);
    }
}

5. Static Using Syntax

C# 5.0 Version

public class RegistrationPage
{
    private readonly IWebDriver driver;
    private readonly string url = @"http://www.automatetheplanet.com/register";
    public RegistrationPage(IWebDriver browser)
    {
        this.driver = browser;
        PageFactory.InitElements(browser, this);
    }
    [FindsBy(How = How.Id, Using = "emailId")]
    public IWebElement Email { get; set; }
    [FindsBy(How = How.Id, Using = "passId")]
    public IWebElement Pass { get; set; }
    [FindsBy(How = How.Id, Using = "userNameId")]
    public IWebElement UserName { get; set; }
    [FindsBy(How = How.Id, Using = "registerBtnId")]
    public IWebElement RegisterButton { get; set; }
    public User RegisterUser(string email = null, string password = null, string userName = null)
    {
        var user = new User();
        this.driver.Navigate().GoToUrl(this.url);
        if (string.IsNullOrEmpty(email))
        {
            email = UniqueEmailGenerator.BuildUniqueEmailTimestamp();
        }
        user.Email = email;
        this.Email.SendKeys(email);
        if (string.IsNullOrEmpty(password))
        {
            password = TimestampBuilder.GenerateUniqueText();
        }
        user.Pass = password;
        this.Pass.SendKeys(password);
        if (string.IsNullOrEmpty(userName))
        {
            userName = TimestampBuilder.GenerateUniqueText();
        }
        user.UserName = userName;
        this.UserName.SendKeys(userName);
        this.RegisterButton.Click();
        return user;
    }
}

Sometimes we use static utility classes for some common actions. In the above code, we use the static TimestampBuilder to generate a unique text and the UniqueEmailGenerator to create a unique email. However, you need always to specify the name of the class that contains the static methods which make the code harder to read.

C# 6.0 Version

With the new using static syntax, you don't need to specify the name of the static methods' class in front of the methods.

using OpenQA.Selenium;
using OpenQA.Selenium.Support.PageObjects;
using static WebDriverTestsCSharpSix.CSharpSix.StaticUsingSyntax.TimestampBuilder;
using static WebDriverTestsCSharpSix.CSharpSix.StaticUsingSyntax.UniqueEmailGenerator;
namespace WebDriverTestsCSharpSix.CSharpSix.StaticUsingSyntax
{
    public class RegistrationPage
    {
        private readonly IWebDriver driver;
        private readonly string url = @"http://www.automatetheplanet.com/register";
        public RegistrationPage(IWebDriver browser)
        {
            this.driver = browser;
            PageFactory.InitElements(browser, this);
        }
        [FindsBy(How = How.Id, Using = "emailId")]
        public IWebElement Email { get; set; }
        [FindsBy(How = How.Id, Using = "passId")]
        public IWebElement Pass { get; set; }
        [FindsBy(How = How.Id, Using = "userNameId")]
        public IWebElement UserName { get; set; }
        [FindsBy(How = How.Id, Using = "registerBtnId")]
        public IWebElement RegisterButton { get; set; }
        public User RegisterUser(string email = null, string password = null, string userName = null)
        {
            var user = new User();
            this.driver.Navigate().GoToUrl(this.url);
            if (string.IsNullOrEmpty(email))
            {
                email = BuildUniqueEmailTimestamp();
            }
            user.Email = email;
            this.Email.SendKeys(email);
            if (string.IsNullOrEmpty(password))
            {
                password = GenerateUniqueText();
            }
            user.Pass = password;
            this.Pass.SendKeys(password);
            if (string.IsNullOrEmpty(userName))
            {
                userName = GenerateUniqueText();
            }
            user.UserName = userName;
            this.UserName.SendKeys(userName);
            this.RegisterButton.Click();
            return user;
        }
    }
}

To use the new feature, add a using static line that contains the name of the class that holds the static methods. Then you can use them without the name of the class.

using static WebDriverTestsCSharpSix.CSharpSix.StaticUsingSyntax.TimestampBuilder;
using static WebDriverTestsCSharpSix.CSharpSix.StaticUsingSyntax.UniqueEmailGenerator;
//....
public User RegisterUser(string email = null, string password = null, string userName = null)
{
    //..
    if (string.IsNullOrEmpty(email))
    {
        email = BuildUniqueEmailTimestamp();
    }
    //..
    return user;
}

6. String Interpolation

C# 5.0 Version

public class ResourcesPage
{
    private readonly IWebDriver driver;
    private readonly string url = @"https://automatetheplanet.com/resources/";
    public ResourcesPage(IWebDriver browser)
    {
        this.driver = browser;
        PageFactory.InitElements(browser, this);
    }
    public string Url => this.url;
    [FindsBy(How = How.Id, Using = "emailId")]
    public IWebElement Email { get; set; }
    [FindsBy(How = How.Id, Using = "nameId")]
    public IWebElement Name { get; set; }
    [FindsBy(How = How.Id, Using = "downloadBtnId")]
    public IWebElement DownloadButton { get; set; }
    [FindsBy(How = How.Id, Using = "successMessageId")]
    public IWebElement SuccessMessage { get; set; }
    public IWebElement GetGridElement(string productName, int rowNumber)
    {
        var xpathLocator = string.Format("(//span[text()='{0}'])[{1}]/ancestor::td[1]/following-sibling::td[7]/span", productName, rowNumber);
        return this.driver.FindElement(By.XPath(xpathLocator));
    }
    public void Navigate() => this.driver.Navigate().GoToUrl(this.url);
    public void DownloadSourceCode(string email, string name)
    {
        this.Email.SendKeys(email);
        this.Name.SendKeys(name);
        this.DownloadButton.Click();
        var successMessage = string.Format("Thank you for downloading {0}! An email was sent to {1}. Check your inbox.", name, email);
        var waitElem = new WebDriverWait(driver, TimeSpan.FromSeconds(30));
        waitElem.Until(ExpectedConditions.TextToBePresentInElementLocated(By.Id("successMessageId"), successMessage));
    }
    public void AssertSuccessMessage(string name, string email)
    {
        var successMessage = string.Format("Thank you for downloading {0}! An email was sent to {1}. Check your inbox.", name, email);
        Assert.AreEqual(successMessage, this.SuccessMessage.Text);
    }
}

There are lots of cases where we use string.Format in our tests' code. In the example above, there are three places- generation of a unique XPath locator, generation of the success message that we need to wait for, generation of the success message that we will assert. I am so used to this syntax that it doesn't bother me anymore however it 's hard to read. Moreover, if you delete one of the parameters of the string.Format there won't be any compilation error. Instead, you will get a run-time one.

C# 6.0 Version

In C# 6.0, we have a cleaner way to format a string by writing our arguments instead of referring to them as placeholders. Just make sure you use the $ before the start of the string.

public class ResourcesPage
{
    private readonly IWebDriver driver;
    private readonly string url = @"https://automatetheplanet.com/resources/";
    public ResourcesPage(IWebDriver browser)
    {
        this.driver = browser;
        PageFactory.InitElements(browser, this);
    }
    public string Url => this.url;
    [FindsBy(How = How.Id, Using = "emailId")]
    public IWebElement Email { get; set; }
    [FindsBy(How = How.Id, Using = "nameId")]
    public IWebElement Name { get; set; }
    [FindsBy(How = How.Id, Using = "downloadBtnId")]
    public IWebElement DownloadButton { get; set; }
    [FindsBy(How = How.Id, Using = "successMessageId")]
    public IWebElement SuccessMessage { get; set; }
    public IWebElement GetGridElement(string productName, int rowNumber)
    {
        var xpathLocator = $"(//span[text()='{productName}'])[{rowNumber}]/ancestor::td[1]/following-sibling::td[7]/span";
        return this.driver.FindElement(By.XPath(xpathLocator));
    }
    public void Navigate() => this.driver.Navigate().GoToUrl(this.url);
    public void DownloadSourceCode(string email, string name)
    {
        this.Email.SendKeys(email);
        this.Name.SendKeys(name);
        this.DownloadButton.Click();
        var successMessage = $"Thank you for downloading {name}! An email was sent to {email}. Check your inbox.";
        var waitElem = new WebDriverWait(driver, TimeSpan.FromSeconds(30));
        waitElem.Until(ExpectedConditions.TextToBePresentInElementLocated(By.Id("successMessageId"), successMessage));
    }
    public void AssertSuccessMessage(string name, string email)
    {
        var successMessage = $"Thank you for downloading {name}! An email was sent to {email}. Check your inbox.";
        Assert.AreEqual(successMessage, this.SuccessMessage.Text);
    }
}

Online Training

LEVEL: 1

C# Web Test Automation Fundamentals

Learn how to write and maintainable C# tests, understand test automation best practices and fundamental testing concepts.
  • C# Level 1
  • C# Unit Testing Fundamentals
  • Source Control Introduction
  • Selenium WebDriver- Getting Started
  • Setup Continuous Integration Job
Duration:
20 hours
4 hour per day

LEVEL: 2

C# Test Automation Advanced

Learn how to use advanced C# features for creating tests. As well riting tests for WPF, WinForms, Universal using WinAppDriver and Android and iOS using Appium.
  • C# Level 2
  • WebDriver Level 2
  • Appium Level 1
  • WinAppDriver Level 1
  • WebDriver in Docker and Cloud
  • Test Reporting Solutions and Frameworks
  • Behavior-Driven Development- SpecFlow
Duration:
30 hours
4 hour per day

LEVEL: 3

C# Enterprise Test Automation Framework

Learn how to build projects structure as well essential design patterns. Handling environment configurations and framework extensibility.
After discussing the core characteristics, we will start writing the core feature piece by piece. We will continuously elaborate on why we design the code the way it is and look into different designs and compare them.
You will have exercises to finish a particular part or extend it further along with discussing design patterns and best practices in programming.
Duration:
30 hours
4 hour per day

LEVEL: 1

Java Web Test Automation Fundamentals

Learn how to write and maintainable Java tests, understand test automation best practices and fundamental testing concepts.
  • Java Level 1
  • Java Unit Testing Fundamentals
  • Source Control Introduction
  • Selenium WebDriver- Getting Started
  • Setup Continuous Integration Job
Duration:
20 hours
4 hour per day

LEVEL: 2

Java Test Automation Advanced

Learn how to use advanced Java features for creating tests. As well riting tests for WPF, WinForms, Universal using WinAppDriver and Android and iOS using Appium.
  • Java Level 2
  • WebDriver Level 2
  • Appium Level 1
  • WinAppDriver Level 1
  • WebDriver in Docker and Cloud
  • Test Reporting Solutions and Frameworks
  • Behavior-Driven Development
Duration:
30 hours
4 hour per day

LEVEL: 3

Java Enterprise Test Automation Framework

Learn how to build projects structure as well essential design patterns. Handling environment configurations and framework extensibility.
After discussing the core characteristics, we will start writing the core feature piece by piece. We will continuously elaborate on why we design the code the way it is and look into different designs and compare them.
You will have exercises to finish a particular part or extend it further along with discussing design patterns and best practices in programming.
Duration:
30 hours
4 hour per day

Performance Testing

Thе course is designed to teach QAs in tutorial format how to plan and conduct a performance test.
  • Fundamentals of Performance Testing
  • Key Factors and Reference Technologies for Performance Testing
  • Fundamentals of Web Technologies
  • Web Debugging with Chrome DevTools, Postman, and Fiddler
  • Performance Testing with WebPageTest
  • Introduction to Jmeter
  • Creating and Executing Jmeter Tests
  • Test Definition, Design, and Plan in Performance Testing
  • Setup of Test Environment
  • CI Integration of Performance and Load Testing
  • Test Execution and Results Analysis
  • Monitoring and Control
Duration:
24 hours
8 hour per day

Download full source code

Related Articles

Web Automation

Advanced Web UI Components Automation with WebDriver C#

Most of the websites out there use commercial Web UI Components for their front end.  Most of these components are JavaScript-based, and their test automation is a little bit trickier. In this article, I am going

Web Automation

Page Object Model Design Pattern Using xUnit Part 8

This video helps understand Page Object design pattern in Selenium C#. Along with deep-diving into how to use the Page Objects, demonstrates the usage of POM (Page Object Model) to run the tests in the Selenium

Web Automation

How To Perform Geolocation Testing Using xUnit Part 7

In this explains Geolocation testing using xUnit with practical implementation. If you build consumer web products for different audiences, geolocation testing becomes necessary because a web application or a website may behave differently if viewed from

Web Automation

How To Perform Cross Browser Testing Using xUnit Part 6

The video explains cross browser testing using xUnit with practical examples. It guides you the advanced concepts of Selenium test automation.Previous X-Unit Tutorial Episodes Introduction to XUnit.net Tutorial for Beginners Part 1 How To Set Up

Web Automation

How To Run Parallel Test Using xUnit WebDriver Part 5

Learn how to run your WebDriver tests in parallel using xUnit and properly integrate both testing libraries.Previous X-Unit Tutorial Episodes Introduction to XUnit.net Tutorial for Beginners Part 1 How To Set Up xUnit With Visual Studio?

Web Automation

Parameterized Tests In xUnit WebDriver Part 4

This video is the next part of our XUnit Tutorial for Beginners. The video explains the use of xUnit with the help of examples showcasing how to write parameterized tests in xUnit Selenium C#.Previous X-Unit Tutorial

About the author

CTO and Co-founder of Automate The Planet Ltd, inventor of BELLATRIX Test Automation Framework, author of "Design Patterns for High-Quality Automated Tests: Clean Code for Bulletproof Tests" in C# and Java. Nowadays, he leads a team of passionate engineers helping companies succeed with their test automation. Additionally, he consults companies and leads automated testing trainings, writes books, and gives conference talks.