Developer Tips: C# Selenium with MSTest Basics

Our guest blogger, Andrew Hinkle, walks us through a checklist of how to setup Selenium with an introduction to DogFoodCon 2019

Written by Andrew Hinkle • Last Updated: • Develop •
Quiet Please *Testing*

Selenium is a great tool for testing your user interface (UI). There are plenty of great tutorials on the web that I encourage you to review. This article is going to cover some basic setup steps, a simple .NET Core 2.1 code sample, and promote additional resources such as an intermediate session you may attend at Dog Food Con 2019 to learn how to incorporate your Selenium tests into your DevOps pipeline.

Browser Settings

  1. Assumptions: Chrome/Firefox (64-bit)/IE11/Edge (Win10 or higher).
  2. Most of these settings have to be done for IE11 as the modern browsers do this by default or the alternative usually still works.
    1. Always open pop-ups in a new tab.
    2. Turn off pop-up blockers.
    3. IE11: Enable Protected Mode for all security zones.
    4. Disable save password prompts.
    5. When prompted to AutoComplete, click "No".
    6. Set zoom to 100%.
  3. Restart the browsers

Windows Settings

  1. Disable the logon screen saver so while you're sitting back watching your automated tests run the screen saver does not kick on ruining your test.
  2. Restart the computer

Set the WebDrivers

  1. IE 11
    1. https://www.seleniumhq.org/download/
    2. Under "The Internet Explorer Driver Server" section > click "32 bit Windows IE"
  2. 64 bit should also work, but some consultants I worked with recommended the 32 bit over 64 bit as of 12/2018
    1. Extract "IEDriverServer.exe" from the zip to c:\Selenium.WebDrivers
  3. Microsoft Edge (EdgeHtml)
    1. https://developer.microsoft.com/en-us/microsoft-edge/tools/webdriver/
    2. Edge version 18 or great, then run the following in command prompt as an administrator
      1. DISM.exe /Online /Add-Capability /CapabilityName:Microsoft.WebDriver~~~~0.0.1.0
      2. Edge version less than 18, then do the following
    3. Under "Downloads" > Microsoft Edge (EdgeHtml) > click the top Release #####
      1. Save "MicrosoftWebDriver.exe" to c:\Selenium.WebDrivers
  4. Microsoft Edge (Chromium)
    1. Since this version is in Preview  I did not download and test but here are the steps
    2. https://developer.microsoft.com/en-us/microsoft-edge/tools/webdriver/
    3. Under "Downloads" > Microsoft Edge (Chromium) > for the top Release ##### click x64
    4. Extract "msedgedriver.exe " from the zip to c:\Selenium.WebDrivers
  5. Chrome
    1. https://sites.google.com/a/chromium.org/chromedriver/
    2. Under "All versions available in Downloads" next to the "Latest stable release" click the ChromeDriver #.## link > Click "chromedriver_win32.zip"
    3. Extract "chromedriver.exe" from the zip to c:\Selenium.WebDrivers
  6. Firefox
    1. https://github.com/mozilla/geckodriver/releases
    2. Under the latest release v#.##.# under "Assets" click the geckodriver-*-win64.zip
    3. Extract "geckodriver.exe" from the zip to c:\Selenium.WebDrivers

Create the Application

The full source code is in https://github.com/penblade/Tips/tree/master/Tips.Selenium.

  1. Create a new project > MSTest Test Project (.NET Core)
  2. Install the following NuGet packages
    1. WaitHelpers by SeleniumExtras.WaitHelpers (v3.11.0)
      1. Used for StalenessOf checks
    2. Support by Selenium Committers (v3.141.0)
    3. WebDriver by Selenium Committers (v3.141.0)

src/Test/Utilities/BrowserType.cs

namespace OpenQA.Selenium
{
    public enum BrowserType
    {
        NotSet,
        Chrome,
        Firefox,
        Edge,
        IE11
    }
}

WebDriverFactory

Create a factory to get the correct browser web driver.

using System;
using OpenQA.Selenium.Chrome;
using OpenQA.Selenium.Edge;
using OpenQA.Selenium.Firefox;
using OpenQA.Selenium.IE;

namespace
 OpenQA.Selenium {     internal static class WebDriverFactory     {         public static IWebDriver Create(BrowserType browserType, string seleniumWebDriversPath)         {             switch (browserType)             {                 case BrowserType.Chrome:                     return new ChromeDriver(seleniumWebDriversPath);                 case BrowserType.Firefox:                     return new FirefoxDriver(seleniumWebDriversPath);                 case BrowserType.Edge:                     // Edge 18 or greater is installed via command line.  See docs for more info.                     return new EdgeDriver();                 case BrowserType.IE11:                     return new InternetExplorerDriver(seleniumWebDriversPath);                 default:                     throw new ArgumentOutOfRangeException(nameof(browserType), browserType, null);             }         }     } }

WebDriverExtensions

I toiled over deciding if I wanted to create class objects vs. extension methods off the IWebDriver.  After going back and forth multiple times I settled on the extension methods.  After some further research, I decided to follow the naming convention of not including the "I" in front of the extensions class.  I decided to keep the code in the OpenQA.Selenium namespace except for the actual test class, so developers would not have to add another using path.

using System;
using System.Diagnostics;
using OpenQA.Selenium.Support.UI;
using ExpectedConditions = SeleniumExtras.WaitHelpers.ExpectedConditions;

namespace
 OpenQA.Selenium {     internal static class WebDriverExtensions     {         // Consider storing the DefaultWaitTime in the web.config.         private const int DefaultWaitTime = 10;         // Create a default wait time span so we can reuse the most common time span.         private static readonly TimeSpan DefaultWaitTimeSpan = TimeSpan.FromSeconds(DefaultWaitTime);
        public static IWait<IWebDriver> Wait(this IWebDriver driver) => Wait(driver, DefaultWaitTimeSpan);         public static IWait<IWebDriver> Wait(this IWebDriver driver, int waitTime) => Wait(driver, TimeSpan.FromSeconds(waitTime));         public static IWait<IWebDriver> Wait(this IWebDriver driver, TimeSpan waitTimeSpan) => new WebDriverWait(driver, waitTimeSpan);
        public static IWebElement WaitUntilFindElement(this IWebDriver driver, By locator)         {             driver.Wait().Until(condition => ExpectedConditions.ElementIsVisible(locator));             return driver.FindElement(locator);         }
        public static IWebElement WaitUntilFindElement(this IWebDriver driver, By locator, Func<IWebDriver, IWebElement> condition)         {             driver.Wait().Until(condition);             return driver.FindElement(locator);         }
        public static IWebElement WaitUntilInitialPageLoad(this IWebDriver driver, string titleOnNewPage)         {             driver.Wait().Until(ExpectedConditions.TitleIs(titleOnNewPage));             return driver.WaitUntilFindElementForPageLoadCheck();         }
        public static IWebElement WaitUntilPageLoad(this IWebDriver driver, string titleOnNewPage, IWebElement elementOnOldPage)         {             // Inspiration:             // http://www.obeythetestinggoat.com/how-to-get-selenium-to-wait-for-page-load-after-a-click.html             // https://stackoverflow.com/questions/49866334/c-sharp-selenium-expectedconditions-is-obsolete             driver.Wait().Until(ExpectedConditions.StalenessOf(elementOnOldPage));             driver.Wait().Until(ExpectedConditions.TitleIs(titleOnNewPage));             return driver.WaitUntilFindElementForPageLoadCheck();         }
        private static IWebElement WaitUntilFindElementForPageLoadCheck(this IWebDriver driver) => driver.WaitUntilFindElement(By.XPath("html"));
        public static void ScrollIntoView(this IWebDriver driver, IWebElement element)         {             // Assumes IWebDriver can be cast as IJavaScriptExecuter.             ScrollIntoView((IJavaScriptExecutor) driver, element);         }
        private static void ScrollIntoView(IJavaScriptExecutor driver, IWebElement element)         {             // The MoveToElement does not scroll the element to the top of the page.             //new Actions(driver).MoveToElement(session).Perform();             driver.ExecuteScript("arguments[0].scrollIntoView(true);", element);         }
        public static void Quit(this IWebDriver driver, BrowserType browserType)         {             driver.Quit();             if (browserType != BrowserType.IE11) return;
            EndProcessTree("IEDriverServer.exe");             EndProcessTree("iexplore.exe");         }
        private static void EndProcessTree(string imageName)         {             // Inspiration             // https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/taskkill             // https://stackoverflow.com/questions/5901679/kill-process-tree-programmatically-in-c-sharp             // https://stackoverflow.com/questions/36729512/internet-explorer-11-does-not-close-after-selenium-test
            // /f - force process to terminate             // /fi <Filter> - /fi \"pid gt 0 \" - select all processes             // /im <ImageName> - select only processes with this image name             Process.Start(new ProcessStartInfo             {                 FileName = "taskkill",                 Arguments = $"/f /fi \"pid gt 0\" /im {imageName}",                 CreateNoWindow = true,                 UseShellExecute = false             })?.WaitForExit();         }     } }

WebDriverTest

Now that the extensions have been setup, let's add our test.  We'll open a web page, click a link, wait until page load, and then scroll to an element.

For the test, I want to loop through the list of browsers as part of the same step to verify I have setup the environment correctly.  Once you start looking into adding tests through the pipeline you'll want to look into using a test runner and specify the browser to test at run time via a configuration file.

using Microsoft.VisualStudio.TestTools.UnitTesting;
using OpenQA.Selenium;

namespace
 Tips.Selenium.Test {     [TestClass]     public class WebDriverTest     {         private const string SeleniumWebDriversPath = @"C:\Selenium.WebDrivers";         private const string TestUrl = @"https://dogfoodcon.com/";
        [TestMethod]         [DataRow(BrowserType.Chrome, SeleniumWebDriversPath)]         [DataRow(BrowserType.Firefox, SeleniumWebDriversPath)]         [DataRow(BrowserType.Edge, SeleniumWebDriversPath)]         [DataRow(BrowserType.IE11, SeleniumWebDriversPath)]         public void VerifyRemoteWebDriversAreSetup(BrowserType browserType, string seleniumWebDriversPath)         {             using (var driver = WebDriverFactory.Create(browserType, seleniumWebDriversPath))             {                 driver.Navigate().GoToUrl(TestUrl);
                // DogFoodCon                 var pageLoadCheck = driver.WaitUntilInitialPageLoad("DogFoodCon");
                var sessionsLink = driver.WaitUntilFindElement(By.XPath("//a[@title='Sessions']"));                 sessionsLink.Click();
                // Sessions - DogFoodCon                 pageLoadCheck = driver.WaitUntilPageLoad("Sessions – DogFoodCon", pageLoadCheck);
                var session = driver.WaitUntilFindElement(By.XPath("//a[text()='Jeff McKenzie']"));
                // Scroll to the element, but don't verify it is visible to the user.                 // I did this step just so you can see the session appear on the screen.                 driver.ScrollIntoView(session);
                driver.Quit(browserType);             }         }     } }

Resources

  1. The full source code is in https://github.com/penblade/Tips/tree/master/Tips.Selenium
  2. Introduction To Selenium Webdriver With C# In Visual Studio 2015
  3. Migrating A Selenium Project From .NET Framework To .NET Core
  4. Most Complete Selenium WebDriver C# Cheat Sheet
  5. DogFoodCon 2018 – Addition by Abstration: A conversation about abstraction and dynamic programming – by Jerren Every
    1. Jerren discussed standard refactoring techniques with Test-Driven Development along with concepts like Page Objects that abstract away logic from data.
  6. Here are some additional Notes on a couple issues I dealt with on setup with potential solutions
    1. My Win10 Chrome kept displaying a Windows Defender message
    2. You're supposed to add your Selenium driver path to the environment variable %PATH%, but I hit the max character limit

DogFoodCon 2019

DogFoodCon is my favorite conference to attend each year which is why I was ecstatic when I was approached to become a volunteer tech blogger.  I've written a few articles covering the sessions I've attended so far at DogFoodCon 2017 and DogFoodCon 2018.

Unlike those articles that reviewed a session I've attended, I'll be writing a few articles like this one that will provide some context (when possible) into one of the sessions with the goal to spur some interest and lead those of you with interest into attending the session.  Let us know in the comments your thoughts on this type of article.  Thanks!

If you followed the source code above or downloaded and ran it yourself, you'll see that the Selenium code brought up the following session.

DogFoodCon > Sessions > Jeff McKenzie[DevOps] Get Some UI In Your CI

I had a delightful interview with Jeff McKenzie where we discussed the highlights of his session.  While there may be a short intro to Selenium, this is an intermediate level session so there is assumed basic knowledge of how to create a Selenium test in Java or C#.  We compared and contrasted techniques we've both implemented in our pipelines.  Jeff will be providing an overview of the following workflows:

  1. Java > Gherkins/Cucumber > Selenium > Jenkins
    1. 2 years of experience on various projects
  2. C# .NET > SpecFlow > Selenium > Azure DevOps
    1. 5 years of experience on various projects

My team currently works with the workflow: C# .NET > MSTest (Page Object Model) > Selenium > Azure DevOps.  I found a lot of common ground and can't wait to compare his process fully with ours.

Jeff McKenzie has been working with Azure DevOps in its various forms since its inception and over the last couple years has translated that knowledge and experience to the Jenkins workflow as well.  Testing is one of his passions as evidenced by his dedication to following Test-Driven Development (TDD) (15 years) and Behavior-Driven Development (BDD) (7 years).

By attending this session, you'll learn tried and true patterns to develop the mentioned workflows for testing UI in your DevOps processes.

Special thanks to our Adamantium Sponsor

Insight Digital Innovation

  • Insight Digital Innovation's Twitter Page
  • Insight Digital Innovation's LinkedIn Company Profile

Conclusion

I've provided the basics required to setup Selenium including the environment for Win10 and browsers.  The sample code demonstrates how to implement common calls to handle waits, find elements, and wait until page load as extension methods off the IWebDriver to enhance and stream line the process.  Next steps would be to learn about the Page Object models.  I also plugged a DogFoodCon 2019 session that will teach you how to add your Selenium tests into your DevOps pipeline.

Did I cover Selenium basics?  Did I miss any?  Did the setup instructions work for you?  Do you like the extension methods implementation or do you prefer creating class objects instead?

Did you like the plug for the DogFoodCon session?  Would the conference/session get as much attention if it was its own article?  Do you like the format of this article?  What can I do to improve these articles?

ASP.NET 8 Best Practices on Amazon

ASP.NET 8 Best Practices by Jonathan Danylko


Reviewed as a "comprehensive guide" and a "roadmap to excellence" with over 120 Best Practices for ASP.NET Core 8, Jonathan's first book by Packt Publishing explores proven techniques for every phase of the SDLC.

Learn industry-standard concepts to improve your coding, debugging, and deployment of ASP.NET Core websites.

Order now on Amazon.com button

Picture of Andrew Hinkle

Andrew Hinkle has been developing applications since 2000 from LAMP to Full-Stack ASP.NET C# environments from manufacturing, e-commerce, to insurance.

He has a knack for breaking applications, fixing them, and then documenting it. He fancies himself as a mentor in training. His interests include coding, gaming, and writing. Mostly in that order.

comments powered by Disqus