In this post, I’ll focus on an ecosystem I haven’t given much attention: that of Windows and the use .NET related technologies. I’ll be covering the use of Selenium WebDriver within a C# context.
Many testers find themselves in Windows shops where there is preference for Visual Studio, Team Foundation Server and other Microsoft technologies. In those contexts, using C# as part of a test solution is not only justified but often mandated. The good news here is that, Windows-haters aside, Visual Studio is one of the best — if not the best — IDE on the market today. C# is an incredibly powerful language, learning much from Java and expanding upon it.
Even if you are someone who works in Java, Python or Ruby contexts, it can be good for you to see that test solutions like Selenium WebDriver are more than viable in a Windows context.
Getting Started
Before we get started, you’ll need to get the relevant parts of the ecosystem set up for you. I recommend reading the “Setting Up C#” section on my automation setup page. As mentioned there, since Visual Studio has fully functional free editions, in this post I’ll be assuming that you are using it.
Also, before getting started on the project, make sure you have a Firefox browser. Selenium is configured to work with Firefox by default. Selenium does support multiple drivers for different browsers and I’ll show you that in due course, but for now we’ll stick with minimum dependencies in our project.
First I’m going to take you through a very simple example using a console project, just to get you up and running and comfortable. Then we’ll move into a specific test project.
Create Console Project
In Visual Studio, create a new project (using the menu path File –> New –> Project). From the dialog, choose ‘Console Application’ (under Templates | Visual C# | Windows). Call the project SymbioteTest.
That’s going to give you a project with a Program.cs file. We’ll get into the contents of that shortly. For now, in the Solution Explorer (which, by default, is on the right side of your Visual Studio interface), right click on your project name and select ‘Manage NuGet Packages’. In the dialog that appears, search for the term “Selenium”. In the list that appears, you should see both Selenium.WebDriver and Selenium.Support. Install both, choosing the latest stable version of each (which should be the default).
After installation, you can see WebDriver and WebDriver.support in the References section in the Solution Explorer. You’ll also see a file called packages.config. In that file, you’ll see something like the following:
1 2 3 4 |
<packages> <package id="Selenium.Support" version="2.48.0" targetFramework="net452" /> <package id="Selenium.WebDriver" version="2.48.0" targetFramework="net452" /> </packages> |
Now let’s consider Program.cs. Your default file should look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace SymbioteTest { class Program { static void Main(string[] args) { } } } |
So let’s get started putting our own logic in place. At the end of the set of “using” statements, add the following:
1 2 |
using OpenQA.Selenium; using OpenQA.Selenium.Firefox; |
This will allow you to use the Selenium components we just grabbed with NuGet. We’re going to instantiate an instance of the WebDriver interface. Add the following line to the Main method:
1 2 3 4 5 6 7 |
class Program { static void Main(string[] args) { IWebDriver driver = new FirefoxDriver(); } } |
The IWebDriver interface allows you to not only control the browser by creating a particular browser instance (in this case, Firefox) but also to interact with web elements that appear in the browser. We’re going to use the instance we just created to navigate to a particular page. Add the following:
1 2 3 4 5 6 7 8 |
class Program { static void Main(string[] args) { IWebDriver driver = new FirefoxDriver(); driver.Navigate().GoToUrl("http://symbiote-app.herokuapp.com/"); } } |
Now let’s see if it works! In your toolbar, you’ll see “Start” with a green arrow next to it. You can also go to the Debug menu and click Start Debugging (F5) or Start Without Debugging (CTRL + F5). This is a console application, remember, so you’ll see a console pop-up and then you should see the Firefox browser open and navigate to the page.
You’ll have to close the browser manually for the time being. Now let’s add two more simple commands just to wrap up this post. Modify your Program.cs as follows:
1 2 3 4 5 6 7 8 9 10 |
class Program { static void Main(string[] args) { IWebDriver driver = new FirefoxDriver(); driver.Navigate().GoToUrl("http://symbiote-app.herokuapp.com/"); driver.Manage().Window.Maximize(); driver.Quit(); } } |
This just shows you that you can control aspects of the browser itself. Here I’m maximizing the browser window and then I’m immediately quitting the browser instance. Note that I’m quitting rather than just closing. You will see code examples with driver.Close();
. The problem with the latter is that while it will close down the current browser instance it will not necessary free up that instance and shut down WebDriver’s connection to the browser. So make sure you Quit() if that’s actually what you want to do.
Create a Test Project
So now let’s create an entirely new project. Once again do the File –> New –> Project thing but this time, choose ‘Unit Test Project’ which you’ll find under Templates –> Visual C# –> Test. Give the project a name Symbiote.
With the project you created, you’ll get a file called UnitTest1.cs with the following contents:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
using System; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Symbiote { [TestClass] public class UnitTest1 { [TestMethod] public void TestMethod1() { } } } |
Rename the file UnitTest1.cs to Login.cs. That should force the public class called UnitTest1 to Login.
Add WebDriver into Project
As with the previous project, right click on the project name and select ‘Manage NuGet Packages’ and then select the same ones you did before: Selenium.WebDriver and Selenium.Support. Also do a search for “Selenium Chrome” and then install Selenium.WebDriver.ChromeDriver. With the last one being included, you will see chromedriver.exe into your project.
The Test Runner
Notice this key line in the code that was generated for you:
1 |
using Microsoft.VisualStudio.TestTools.UnitTesting; |
This is the built in unit testing framework in Visual Studio. It is quite possible to use NUnit as well. Let’s first use the built in framework and then we’ll see about switching to NUnit in another post.
This UnitTest namespace has some test attributes that we’re going to use. You already saw two of the attributes in the code generated for you: [TestClass] and [TestMethod]. If you’ve worked with Java, these are very similar to annotations. The [TestClass] attribute is required in the Microsoft Unit Test Framework for any class that contains test methods. The [TestMethod] attribute is used to indicate specific methods inside a [TestClass] as containing code that must be run during a test execution.
For now let’s delete that existing [TestMethod] since ultimately we’re going to write our own logic. Your logic should look like this:
1 2 3 4 5 6 7 8 9 10 |
using System; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Symbiote { [TestClass] public class Login { } } |
Test Explorer
Visual Studio has a test window that let’s you see at least some details about tests in a given file. Under the Test menu, go to Windows and select Test Explorer. This will open up a panel (by default on the left) that will show you all of the tests you have in the file that is currently being viewed. Specifically, it will show any methods that have the attribute [TestMethod].
Given that we just removed the only such method, this view will currently show you nothing but I’ll refer to it again once we put some logic in place.
Starting to Code
As we did with the our first example, we’re going to add some “using” statements. Add the following to the top of the code:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
using System; using Microsoft.VisualStudio.TestTools.UnitTesting; using OpenQA.Selenium; using OpenQA.Selenium.Firefox; using OpenQA.Selenium.Chrome; namespace Symbiote { [TestClass] public class Login { } } |
Now let’s consider what our test is going to do. I want to go to my Symbiote App launch page. I then went to click the Login pull down (at the top of the screen) and login as an administrator user, which simply means a login of “admin” and a password of “admin”. Once I do that, I’ll be taken to the home page of the application that has a navigation list. That navigation list is one way that I’ll know I logged in correctly.
As far as logging in the first thing I need to do is start a browser and call up the site. When the test is done, I’m going to want to clean up by closing the browser and quitting the WebDriver instance.
Initialize and Cleanup Test
Let’s kick the test off by starting up the browser. We’ll start with the Firefox browser and then try the same thing in the Chrome browser. As in the first script we worked on, you initialize the IWebDriver interface. We’ll be doing this in the [TestClass]. However, initializing the browser is something we might want to do with multiple tests. So we’re going to create a special method for this. Likewise, closing the browser down is something that multiple tests may have to rely on. So add the following to your Login.cs logic:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
namespace Symbiote { [TestClass] public class Login { IWebDriver driver; string url = "http://symbiote-app.herokuapp.com/"; [TestInitialize] public void TestSetup() { driver = new FirefoxDriver(); driver.Navigate().GoToUrl(url); } [TestCleanup] public void Cleanup() { driver.Quit(); } } } |
I’ve declared my IWebDriver instance variable and a URL for the site. I’ve done this outside of any methods so that I can use these in various methods. he [TestInitialize] attribute can be used to any method which you want to runs prior to running any [TestMethod] attribute methods. Likewise, the [TestCleanup] attribute method is what you want to run after [TestMethod] methods.
You’ll notice here that I’m using the FirefoxDriver just as we did in the previous example. This time around, however, we have added the ChromeDriver so if you did want to change this test to run on Chrome, you could just update line 12 from above to read:
1 |
driver = new ChromeDriver(); |
Add Test Logic
Earlier I had you delete the auto-generated attribute [TestMethod] and the associated method. Now we’re going to add that back but, of course, put in our own logic. Any method with the [TestMethod] attribute will contains the actual code. Any such methods will, as I stated before, run after a method with attribute [TestInitialize]. Put the following method in place:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
namespace Symbiote { [TestClass] public class Login { IWebDriver driver; string url = "http://symbiote-app.herokuapp.com/"; [TestInitialize] public void TestSetup() { driver = new FirefoxDriver(); driver.Navigate().GoToUrl(url); } [TestCleanup] public void Cleanup() { driver.Quit(); } [TestMethod] public void LoginAsAdmin() { } } } |
Now with that method in place, let’s build the project. You should be able to go to the Build menu and select Build Solution. You can also just press F6. Make sure you have the Test Explorer window that I mentioned earlier visible. After the build, you will see that the LoginAsAdmin method will now be displayed. This is happening because the method is annotated with the attribute [TestMethod].
To test the code, click on the ‘Run All’ option in the Test Explorer window. You will see new Firefox browser instance launched, the web site will be launched, and then the browser will be closed.
You can use the full power of Visual Studio here. For example, you can run that test in debug mode and you can put break points into your code and step through that. To run in debug mode you need to run test via Test Tool bar. Select the test in Test Explorer and Go to Test -> Debug -> Selected Tests.
Logging In
Now we will work on implementation of test steps themselves. Let’s consider what the login form looks like on my site:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<div id="login" name="login" class="login"> <p id="open">Login</p> <form action="/" method="post"> <p> <label for="username">Username:</label> <input id="username" name="username" type="text"> </p> <p> <label for="password">Password:</label> <input id="password" name="password" type="password"> </p> <p> <input class="site-button" id="login-button" name="button" type="submit" value="Submit"> </p> </form> </div> |
In order to deal with these elements, you’re going to need to use the IWebElement interface. This interface represents any given HTML element on a web page. If you want to operate on something on a web page, you will be doing so in the context of an IWebElement instance. So let’s put the logic in place that will log a user in as the admin:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
namespace Symbiote { [TestClass] public class Login { IWebDriver driver; string url = "http://symbiote-app.herokuapp.com/"; [TestInitialize] public void TestSetup() { driver = new FirefoxDriver(); driver.Navigate().GoToUrl(url); } [TestCleanup] public void Cleanup() { driver.Quit(); } [TestMethod] public void LoginAsAdmin() { driver.FindElement(By.Id("open")).Click(); driver.FindElement(By.Id("username")).SendKeys("admin"); driver.FindElement(By.Id("password")).SendKeys("admin"); driver.FindElement(By.Id("login-button")).Click(); } } } |
Notice the calls to By.Id
which are passed arguments. Those arguments correspond to the id values of the HTML elements that I showed you in the HTML snippet from the Symbiote site.
That code logic should work in that if you run it now, you’ll see the site being logged in to. You’ll also see the test listed in Test Explorer show as green. However, one problem here is that I’m not actually checking for anything. So I don’t really have an assertion and thus I don’t really have a test. I just have a set of actions. As I mentioned, once you login you are on a home page that has a navigation list. So I’ll add a check that looks for that.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
namespace Symbiote { [TestClass] public class Login { IWebDriver driver; string url = "http://symbiote-app.herokuapp.com/"; [TestInitialize] public void TestSetup() { driver = new FirefoxDriver(); driver.Navigate().GoToUrl(url); } [TestCleanup] public void Cleanup() { driver.Quit(); } [TestMethod] public void LoginAsAdmin() { driver.FindElement(By.Id("open")).Click(); driver.FindElement(By.Id("username")).SendKeys("admin"); driver.FindElement(By.Id("password")).SendKeys("admin"); driver.FindElement(By.Id("login-button")).Click(); WebDriverWait wait = new WebDriverWait(driver, TimeSpan.FromSeconds(10)); IWebElement navList = wait.Until<IWebElement>((d) => { return d.FindElement(By.Id("navlist")); }); } } } |
Do note that this will require you adding a “using” statement at the top:
1 |
using OpenQA.Selenium.Support.UI; |
This logic essentially waits ten seconds for a given element (in this case, the one with an id of “navlist”) to appear. If that element does not appear in that time span, then an exception is raised. This is effectively acting as an assertion to say that there was apparently a problem with verifying that you ended up in the right place.
You can do a more traditional assert approach along these lines:
1 |
Assert.AreEqual(false, driver.ElementIsPresent(By.Id("navlist"))); |
That being said, this doesn’t have the benefit of the time waiting aspect. What I showed you earlier was an explicit wait but you can do an implicit wait as well, which is a setting you place on the driver that will work for all cases of elements attempting to be found by WebDriver. Here’s an example of what you could do (likely in your [TestInitialize] code):
1 |
driver.Manage().Timeouts().ImplicitlyWait(new TimeSpan(0, 0, 0, 10)); |
This sets an implicit wait of ten seconds when looking for elements.
Welcome to the .NET Ecosystem!
This post has shown you how to get up and running with some very basic Selenium logic within the context of Visual Studio and C#. I’ll be doing further posts on this in the future. I’ll cover the use of NUnit as well as tools like SpecFlow, which bring some BDD to the C# world.
Hey Jeff,
Thanks for your detailed explanation! This article really helped me to understand and cleared up many concepts. Just 1 question I am having in my mind is can we use multiple methods [Test Method] in c# selenium web driver without using Nunit framework?
Please help me out if it is possible by mentioning some example such as login & forget password. This will get clear my concepts easily and faster.
Thanks in advance! Waiting for your prompt reply!
Thanks,
Nisarg.
I’m not sure I get what you’re asking. [TestMethod] is part of NUnit. It’s effectively an annotation. So if you want multiple of those, you really don’t do that without using the NUnit framework. Here NUnit is just being used as the runner but you can forgo use of the runner and then have methods that you call.
So I guess the thing to keep in mind is that Selenium is just an API, and WebDriver is just a protocol. NUnit is a runner. How you set up your logic to call methods is entirely up to you. You can have multiple methods, for example, and have those methods instantiate a WebDriver instance and a browser driver (FirefoxDriver, ChromeDriver, etc) and then call those methods. In that case, you are calling such methods without using NUnit at all.
Where runners like NUnit or JUnit are helpful is that they let you provide a series of annotations that specify certain methods (like, say, setup()) will occur before any test methods (using Selenium) are called. But there’s nothing stopping you from creating methods and calling those methods such that they instantiate a WebDriver instance, then instantiate a browser driver, and finally call methods on the Selenium API.
I’m probably not answering your query at all. I’m certainly happy to try clarifying further.
Thank you so much