Improving Reliability with Selenium for Web Test Automation

How to make your Selenium Tests less flaky and more reliable through multiple Test Suites?

Improving Reliability with Selenium for Web Test Automation
Photo by Christina @ / Unsplash

At work, among other tools we use dotnet, C# and Selenium to write Functional Tests for Web Applications. For Mobile ones, we’ve been experimenting with Appium to a varying degree of success.

The nature of Web Interfaces is that they are asynchronous or non-blocking. This means you can click on a button resulting in an event while you can do something else. In effect, the page won’t be blocked.

With a rising amount of modern Web Applications using Single Page Applications (SPAs) with distributed backend APIs, the way data is requested, manipulated and displayed is different from traditional applications - whether Desktop ones or Web Monolithic ones such as Ruby on Rails and ASP.NET MVC.

As a result, this brings more uncertainty and less reliability.

What is Selenium?

Selenium is a framework for Automating Browsers using the WebDriver standard. Its most common usage is in Test Automation thanks to the ease of setting up and running tests.

Selenium is an umbrella project for a range of tools and libraries that enable and support the automation of web browsers.

How to handle UI changes?

UI / UX design wireframing with tablet
Photo by Sahand Babali / Unsplash

This is actually a challenge with most popular Web Test automation frameworks. With the asynchronous design of the web, it’s complex to have reliable tests while these tools are mostly synchronous.

For instance, you wait for an element to appear on the screen before performing an action and finally asserting an expected result. However, we cannot guarantee this element to always appear.

One example is because this element depends on a network request to a third-party API which can be down. As a result, it doesn’t appear. So should the test pass or fail?

Selenium 3 vs Selenium 4

For a functional test automation project, I’m using:

  • dotnet 6
  • C# 10
  • Selenium 3.x
  • Page Factory package

We use this package which is based on The Page Object Model Design Pattern as it provides a lot of benefits namely:

  • easy to maintain pages
  • DRY elements
  • elements can be reused throughout the project
  • easy to read as the pages, actions and tests are separated

The most considerable improvement is Selenium 4 now uses the W3C WebDriver standard instead of the legacy JSON Wire Protocol.

So you might be asking yourself, why use Selenium 4 if your project already contains the earlier version and is stable?

As shown in this diagram from QACraft, Selenium 3 uses an intermediate layer to communicate to the browser drivers while Selenium 4 directly communicates with them.

Here’s a list of benefits of using this version:

  • use of WebDriver manager so that you don’t need to manually download the latest executable for an individual browser (this can be done with a single line of code)→ WebDriverManager.chromedriver.setup();
  • new relative locators that are easier for locating sibling and/or child/parent elements
  • capture specific web elements vs the whole page
  • native support for Browser DevTools
  • more secure and stable as most modern browsers no longer support Selenium 3

The most significant change if you're updating your framework is how the desired capabilities are defined.


DesiredCapabilities caps = new DesiredCapabilities();
caps.SetCapability("browserName", "firefox");
caps.SetCapability("platform", "Windows 10");
caps.SetCapability("version", "92");
caps.SetCapability("build", myTestBuild);
caps.SetCapability("name", myTestName);
var driver = new RemoteWebDriver(new Uri(CloudURL), caps);


var browserOptions = new FirefoxOptions();
browserOptions.PlatformName = "Windows 10";
browserOptions.BrowserVersion = "92";
var cloudOptions = new Dictionary<string, object>();
cloudOptions.Add("build", myTestBuild);
cloudOptions.Add("name", myTestName);
browserOptions.AddAdditionalOption("cloud:options", cloudOptions);
var driver = new RemoteWebDriver(new Uri(CloudURL), browserOptions);

Waiting Strategies

The use of waiting strategies is fundamental to making your tests less flaky and more reliable. However, there’s no perfection in this aspect. Tests will fail due to variations of latency in the network connection. One way of making your tests more robust is using Retry policies provided by your Test Runners.

Implicit Wait

With the use of Selenium, implicit wait is the most important parameter in my opinion. Otherwise, the WebDriver locating elements will be a hit or miss especially if a page takes time to completely load.

You can use Thread.Sleep but it’s not recommended and will unnecessarily delay your overall testing suite execution time.

Driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(5);

Explicit Wait

With Explicit waits, the program execution will pause execution until the passed condition is resolved. Otherwise, there’s a timeout. In this case, as well, I’ve noticed that if you add too long of a timeout, your overall test suite execution time will drastically suffer.

WebDriverWait wait = new WebDriverWait(driver, TimeSpan.FromSeconds(10));
IWebElement firstResult = wait.Until(e => e.FindElement(By.CssSelector(".container")));

Note that the distinction between Implicit and Explicit wait is that the WebDriver polls the DOM for any element.

The official docs recommend avoiding mixing these two types of waits.

Fluent Wait

From the official docs:

FluentWait instance defines the maximum amount of time to wait for a condition, as well as the frequency with which to check the condition.

Here’s an example of the code:

using (var driver = new FirefoxDriver())
  WebDriverWait wait = new WebDriverWait(driver, timeout: TimeSpan.FromSeconds(30))
      PollingInterval = TimeSpan.FromSeconds(5),

  var foo = wait.Until(drv => drv.FindElement(By.Id("foo")));

C# uses the same WebDriverWait library instead of FluentWait, which is for Java.

I’ve used FluentWait sparingly in my last projects as I think the other waits do their job similarly albeit with fewer boilerplate codes.

P.S: If you find this post useful, I will invite you to share it with your network or with anyone you think it might be valuable.