Click here to Skip to main content
15,881,089 members
Articles / DevOps / Testing

Enhanced Selenium WebDriver Tests with the New Improved C# 6.0

Rate me:
Please Sign up or sign in to vote.
5.00/5 (1 vote)
19 Feb 2017Ms-PL5 min read 8.9K   3   1
Find out how to improve your Selenium WebDriver tests through the new improved C# 6.0. The new features will help you to create more readable tests.

I believe that 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 Bing 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 BingMainPage
{
    private readonly IWebDriver driver;

    public BingMainPage(IWebDriver browser)
    {
        this.driver = browser;
        PageFactory.InitElements(browser, this);
    }

    public string Url
    {
        get
        {
            return @"http://www.bing.com/";
        }
    }

    [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?!

C#
public class BingMainPage
{
    private readonly IWebDriver driver;
    private readonly string url = @"http://www.bing.com/";

    public BingMainPage(IWebDriver browser)
    {
        this.driver = browser;
        PageFactory.InitElements(browser, this);
    }

    public string Url => @"http://www.bing.com/";

    [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

BingMainPage.Map

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

C#
public partial class BingMainPage
{
    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

BingMainPage.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.

C#
public partial class BingMainPage
{
    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"));
}

BingMainPage

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

C#
public partial class BingMainPage
{
    private readonly IWebDriver driver;

    public BingMainPage(IWebDriver browser)
    {
        this.driver = browser;
        PageFactory.InitElements(browser, this);
    }

    public string Url => @"http://www.bing.com/";

    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.

C#
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.

C#
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. 

C#
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.

C#
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 Amazon. 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.

C#
public partial class ShippingAddressPage
{
    private readonly IWebDriver driver;
    private readonly string url = @"http://www.amazon.com/shippingPage";

    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.

C#
public partial class ShippingAddressPage
{
    private readonly IWebDriver driver;
    private readonly string url = @"http://www.amazon.com/shippingPage";

    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

C#
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.

C#
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.

C#
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

C#
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.

C#
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);
    }
}

So Far in the 'Pragmatic Automation with WebDriver' Series

The post Enhanced Selenium WebDriver Tests with the New Improved C# 6.0 appeared first on Automate The Planet.

All images are purchased from DepositPhotos.com and cannot be downloaded and used for free.
License Agreement

License

This article, along with any associated source code and files, is licensed under The Microsoft Public License (Ms-PL)


Written By
CEO Automate The Planet
Bulgaria Bulgaria
CTO and Co-founder of Automate The Planet Ltd, inventor of BELLATRIX Test Automation Framework, author of "Design Patterns for High-Quality Automated Tests: High-Quality Test Attributes and Best Practices" 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. You can find him on LinkedIn every day.

Comments and Discussions

 
SuggestionCode block formatting Pin
Bryian Tan19-Feb-17 6:22
professionalBryian Tan19-Feb-17 6:22 
GeneralRe: Code block formatting Pin
Anton Angelov20-Feb-17 1:06
Anton Angelov20-Feb-17 1:06 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.