Thursday, 17 November 2011

Using Selenium WebDriver with C#, NUnit and the Page Object Pattern

In this quick tutorial I am going to show you how to do the following:

  • Set up Selenium Webdriver test project using the C# implementation
  • Use C# and NUnit to build a test framework
  • Use the page object model to build a simple automation test suit
  • Simple data management


Part One - Setting up the project

  • Create a C# .net class library project using the .Net 3.5 framework

    ..\ NUnit-2.5.10.11092\bin\net-2.0\framework\nunit.framework.dll

     ..\net35\*.dll
  • Add a reference to System.Configuration to the project, and the add an application  configuration file

Your solution should like this:



Part Two – Set up a simple test framework using the page object model

Design patterns such as the page object model take the pain out of planning how to manage and set up tests, they also greatly reduce maintenance. I have chosen the page object model as I believe that it is the easiest to understand and gives a robust framework that requires very little, in UI automation terms, maintenance. Read more about automation design patterns here.

For this example I am using a web application I’m working on at the moment, it’s a simple device to manage restaurant table availability. It consists of an account login page and several account utility pages. In this test I have decided to automate the account login page, and the account home page.

Set up some base classes for the project

Create a class called base.cs which will contain all your common variables, objects, methods, etc. For the moment, we just create a new WebDriver instance


using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Configuration;
using System.Linq;
using System.Text;
using NUnit.Framework;
using OpenQA.Selenium;
using OpenQA.Selenium.Firefox;

namespace Automated
{

    public class Base
    {
        public static IWebDriver driver;
        public static string _baseUrl;
        static Base()
        {
            _baseUrl = ConfigurationManager.AppSettings["baseUrl"];
        }
       
        public void NavigateTo(string url)
        {
            var navigateToThisUrl = _baseUrl + url;
            driver.Navigate().GoToUrl(navigateToThisUrl);
        }
        public void GetDriver()
        {
            //driver = new ChromeDriver();
            driver = new FirefoxDriver();
        }
    }
}

You will notice a reference to the application configuration file we created earlier. In here we are going to put the target url of the application under test. It doesn’t need to go here, but having it here allows us to make this value easily configurable without building a data management framework.

Add the following contents:

  <appSettings>
    <add key="baseUrl" value="http://www.mynewapplication.com" />
  appSettings>


There is also a “navigate to” function. This takes our application base url and tacks on any required paths that we may need to visit directly. We call this method inside the page objects.

For this example I need to create two page object models so I create a class called AccountLogin.cs and AccountHome.cs in a solution folder called Pages. We then model the pages in the classes.

By model, what we are doing is identifying actions and elements on a page and placing them inside a method that can be called on that page object. We may even group together these actions as a part of or full work flow on a page. We may also include in the page object any elements that may serve as assertions.

My two page classes look like this

Pages\AccountLogin.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using OpenQA.Selenium.Firefox;
using OpenQA.Selenium.IE;
using OpenQA.Selenium;

namespace Automated
{
    public class AccountLogin : Base
    {

        public AccountLogin NavigateToLogin()
        {
            NavigateTo("/account/login");
            return new AccountLogin();
        }
      
        public AccountHome LoginAs(string username, string password)
        {
            driver.FindElement(By.Id("email")).SendKeys(username);
            driver.FindElement(By.Id("password")).SendKeys(password);
            driver.FindElement(By.Id("submit")).Submit();
            return new AccountHome();
        }
    }
}

In the accountlogin page I have modelled the login action (LoginAs). This action fills the login fields and then submits the login form, finally returning the proceeding page, account home. I am using the FindElement method with the field element and submit Ids being used as the identifiers.

Pages\AccountHome

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using OpenQA.Selenium;

namespace Automated
{
    class AccountHome:Base
    {

       
        public bool User_Welcome_Name(String userFullName)
        {
            IWebElement userWelcome = driver.FindElement(By.ClassName("Welcome");
            bool result = userWelcome.Text.Contains(userFullName);
            return result;
        }
    }
}

In the account home page model, for this example I have identified an object on the page that will tell me if the login has been successful for the user. I have modelled this object as a method that returns a Boolean value, it will return true if the users details that it finds match those that are expected. I am using the FindElement method of Webdriver to locate the text by finding the css classname “Welcome”, I then look in the text to see if the username is contained within.

Bringing the two page objects together as a test

Where possible I try to marry up a test class to a page object, and the actions within that object, for example, the login test class only contains tests that test the login page. It’s similar to functional decomposition only we are breaking an application down into pages. This keeps the testing simple. Sometimes we may also need to introduce workflow testing where we are testing a flow through several pages.

A test class usually contains a Set Up method, one or more tests, and a Tear Down method. The set up method will usually do something like get the webdriver object and make it available to all tests within the class. The Tear Down will usually tidy up the test environment or reset values for the next set of tests. Either of these can be run globally as either a SetUpFixture or TearDownFeature for all tests, or equally, within each test class to be run before and after each test.

A test procedure using selenium requires the following to happen

  1. Instantiate the WebDriver for the browser you wish to automate. This is what we do in the Set Up method, calling the GetDriver method from the base class. This opens up browser specified.
  2. Navigate to a start page. We call the NavigateTo function in the base class to do this. This Method grabs our base url from the app.config file and appends whatever route that we give to the method. See data management below for how to manage different data types such as URLs.
  3. Execute one or more actions, and assert the outcome of those actions.
  4. Tear Down the WebDriver if required, and run clean up commands.

The first test I have written for this example is a successful login test. It navigates to the account login page, fills in the login fields, and submits the login form; it then waits for the account home page to display before executing a checkpoint action to make sure we are on the right page.

The test class looks like this

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NUnit.Framework;

namespace Automated
{
    [TestFixture]
    class LoginTests : AccountLogin
    {

        [SetUp]
        public void Setup()
        {
            GetDriver();
            driver.Manage().Timeouts().ImplicitlyWait(new TimeSpan(0, 0, 30));
        }

        [Test]
        public void Login_Successful()
        {
            AccountLogin accountLogin = NavigateToLogin();
            // Assert.IsTrue(accountLogin.Sign_Button_Visible());
            AccountHome accountHome = LoginAs("tamgus.bultgreb@yahoo.com", "itsasecret");
            Assert.IsTrue(accountHome.User_Welcome_Name("Tamgus Bultgreb"));

        }

        [TearDown]
        public void TearDown()
        {
            // Some funky stuff here..
        }

    }
}

As you can see, there are some nice hard coded values in the test such as user name and password. These should be extracted out of the test and placed in a test data repository, more about that later.

Running the tests

There are several ways to run the tests, from the command line, with NUnits own UI, or with a test add tool such as test driven. Whilst developing I go for Test Driven as it allows me to run tests quickly from visual studio, but during test runtime, with tests being triggered from a CI server, you need to go down the command line route.

Managing the test data

Your tests will almost always need to have some kind of data to drive them. In the example above we needed to provide a username, a password, and some URLs, we also needed to provide data for some checkpoints.

There are several ways of managing data, each with its own merits, from experience the main mechanisms I have seen successfully used are constant files and datasheets. Constant files are basically static classes that contain all the data that your tests will use assigned to variables. This is a nice way to keep all your test artefacts manageable inside the project. It does mean, though, that data can’t be easily manipulated close to runtime, and if you wish to change data, the test project must be rebuilt. It also stops you from using dynamic data at runtime.

Datasheets give you a lot of flexibility when feeding data into tests, you have the ability to implement different scenarios with the same tests, you can also feed data into your datasheets dynamically, and you can update data at any moment up to the test runtime. However, using datasheets tends to remove some of the robustness of your tests. When you are working with constant files, you are immediately aware of any break in your data, datasheets tend to promote a more “detached” approach to data management which can often lead to incorrect data being used within tests.

When choosing which approach to take you can look at the following

Test flexibility
Data reliability

Test flexibility refers to some of the objectives of your automation efforts. If you wish to have flexibility with the data going into your tests, or you wish a business representative or such to feed in data to your tests, the datasheet route is more favourable. If your data is simple and unchanging, constant files usually give a more stable and robust test project, which should always be one of your main objectives.

Data reliability refers to how “safe” is your data from changing. This is one point that I push more than any in automation. Apart from bad programming, data management is the biggest killer of automation projects. Uncontrollable data will mean lots of failed tests. A good example would not be having complete control over a user in an application, and someone changes the password for that user, or the language that the user sees an application in. This would break your test where you are using either one of those items in the test. In our example above, we have a test called successful login, if someone else has access to the user being used and the data, they could change the password and that successful login test will now fail for data reasons, and not a code bug. This adds to the maintenance debt.

Here is an example of how I can add data values into the base class in the form of constants and use that to drive the tests.

In the base class add test data values to the constructor

public class Base
{
    public static IWebDriver driver;

    public static string _baseUrl, userName, password, userFullName;

    static Base()
    {
        _baseUrl = ConfigurationManager.AppSettings["baseUrl"];
        userName = "livetest999@gmail.com";
        password = "testtest";
        userFullName = "Live Test";        
    }

In the test, you can now reference these values as follows
   
[Test]
public void Login_Successful()
{

    AccountLogin accountLogin = NavigateToLogin();
    Assert.IsTrue(accountLogin.Sign_Button_Visible());
    AccountHome accountHome = LoginAs(userName, password);
    Assert.IsTrue(accountHome.UserWelcomeName(userFullName));

}

If you do go down the datasheet route be prepared to invest time in setting up a data management mechanism in your test suite that can read in and parse data for the tests. Libraries such as FileHelpers can greatly facilitate this.

Ok, that’s it for now. Hopefully you can now set up a test project using the .NET version of WebDriver, implement tests using a common design pattern and implement a simple data management technique. We have not touched upon a lot of UI automation here, just the basics. In the coming months I would like to explore cross browser testing, and localisation testing in more detail.

For more information on NUnit visit their site.

For indepth information on WebDriver visit the Selenium WebDriver Project Wiki.