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?!
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!
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.
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.
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.
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.");
}
}
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.");
}
}
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.
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);
}
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 = @"http://www.amazon.com/shippingPage";
public ShippingAddressPage(IWebDriver browser)
{
this.driver = browser;
PageFactory.InitElements(browser, this);
}
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);
}
}
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