Framework – Your First Framework – Part 4

Creating our framework

In part three, we added some core functionality to our framework in the form of window handling and dispose methods to clear up after ourselves. We also looked at adding some basic Javascript functionality to invoke scripts on our pages. So far we have the very basics of a framework, and we could if we wanted, use it as it is to write tests and tear down. But creating a framework for Selenium and WebDriver should be about making life as simple as possible for yourself when it comes to writing tests, and one of the ways to do that is wrapping existing WebDriver functionality in to easy to use helper libraries.

So for part four, we’re going to be doing just that and looking at writing some useful code to make the way we interact with elements on a page far easier and more user friendly.

Elements you’ll encounter when writing web tests will usually be in the form of text boxes, buttons, checkboxes, dropdown/picklists or radio buttons. While there are definitely more, these are the ones that you will most commonly have to deal with, especially at this early stage of learning Selenium. Despite these having different ways of being interacted with on the web pages we’re testing with, to WebDriver, they can be thought of as being very similar. They all have a locator that we need to find it, and they all have common behaviours associated with them when used in testing such as testing for their visibility, reading any text values, or clicking on them.

Because of these common properties, we’re going to create an interface to serve as the basis of a common element object, and have element type specific classes that inherit from this interface. Why use an interface for this? Well think of the interface like serving as a contract between the main element object (the interface), and all the specific element types that use this main element. It ensures that each element specific class implements the common behaviours and properties.

Let’s start by looking at our interface, IPageField:

using OpenQA.Selenium;

namespace AutomationFramework.Engine.Controls
{
    public interface IPageField<T>
    {
        string Name { get; }
        By FindBy { get; }

        T Get();
        void Set(T value);

        bool Exists();
        bool IsVisible();
    }
}

Not much to it but there might be some code here that doesn’t really make sense, at least if you’re new to coding and C#. IPageField<T>, more specifically the <T> part. This is known as a generic type parameter.¬†A generic type parameter allows you to specify an arbitrary type T to a method at compile-time, without specifying a concrete type in the method or class declaration. In this context, the reason we are using it is because the type we want to use will depend on the element we are creating. A text box will use the type string, but a checkbox will use the type bool, as it will only deal in true or false. When you look at the line “T Get();”, this is again saying that the return type of the Get method will be determined by the element type we call/create.

The common method we want every element to have is a Get and Set using a type unique to that element, as well as an Exists or IsVisible. It’s important to have both, as while we may expect something to exist, it doesn’t necessarily mean it will always be visible, so it’s important we have the capability to deal with either scenario.

Other than that, it’s really simple, right? Nothing intimidating at all. We don’t need to do anything in this interface other than define the methods and properties we wish to use across our elements.

As mentioned before in the article, we’re going to create classes for the more common elements. Textbox, button, checkbox, radio and picklist.

using OpenQA.Selenium;
using System;

namespace AutomationFramework.Engine.Controls
{
    public class Checkbox : IPageField<bool>
    {
        public string Name { get; }
        public By FindBy { get; }

        public Checkbox(string name, By findBy)
        {
            Name = name;
            FindBy = findBy;
        }

        public virtual bool Get()
        {
            if (!IsVisible()) { throw new Exception($"Element {FindBy.ToString()} not found or is not visible"); };

            var element = FindBy.FindElement();

            return element.Selected;
        }

        public virtual void Set(bool value)
        {
            if(value != FindBy.WaitUntilElementIsVisible().Selected)
            {
                FindBy.WaitAndClickElement();
            }
        }

        public virtual bool Exists() => FindBy.DoesElementExist();

        public virtual bool IsVisible() => FindBy.IsElementVisible();
    }
}
using OpenQA.Selenium;
using OpenQA.Selenium.Support.UI;
using System;

namespace AutomationFramework.Engine.Controls
{
    public class Picklist : IPageField<string>
    {
        public string Name { get; }
        public By FindBy { get; }

        public Picklist(string name, By findBy)
        {
            Name = name;
            FindBy = findBy;
        }

        public virtual string Get()
        {
            if (!IsVisible()) { throw new Exception($"Element {FindBy.ToString()} not found or is not visible"); };

            var element = FindBy.FindElement();

            return element.Text;
        }

        public virtual void Set(string value)
        {
            FindBy.WaitUntilElementIsVisible();
            FindBy.FindElement().Clear();
            FindBy.FindElement().SendKeys(value);
        }

        public virtual void SetPicklistItemValue(string value)
        {
            if (!IsVisible()) { throw new Exception($"Element {FindBy.ToString()} not found or is not visible"); };
            var element = FindBy.FindElement();

            if(ValidateItemExistsInPicklist(element, value))
            {
                new SelectElement(element).SelectByValue(value);
            }
        }

        public virtual void SetPicklistItemText(string text)
        {
            if (!IsVisible()) { throw new Exception($"Element {FindBy.ToString()} not found or is not visible"); };
            var element = FindBy.FindElement();

            if (ValidateItemExistsInPicklist(element, text))
            {
                new SelectElement(element).SelectByText(text);
            }
        }

        public virtual bool ValidateItemExistsInPicklist(IWebElement element, string value)
        {
            var values = element.Text;
            if(!values.Contains(value))
            {
                return false;
                throw new Exception($"{value} not found in picklist");
            }

            return true;
        }

        public virtual bool Exists() => FindBy.DoesElementExist();

        public virtual bool IsVisible() => FindBy.IsElementVisible();
    }
}
using OpenQA.Selenium;
using System;

namespace AutomationFramework.Engine.Controls
{
    public class Radio : IPageField<bool>
    {
        public string Name { get; }
        public By FindBy { get; }

        public Radio(string name, By findBy)
        {
            Name = name;
            FindBy = findBy;
        }

        public virtual bool Get()
        {
            if (!IsVisible()) { throw new Exception($"Element {FindBy.ToString()} not found or is not visible"); };

            var element = FindBy.FindElement();

            return element.Selected;
        }

        public virtual void Set(bool value)
        {
            if (value != FindBy.WaitUntilElementIsVisible().Selected)
            {
                FindBy.WaitAndClickElement();
            }
        }

        public virtual bool Exists() => FindBy.DoesElementExist();

        public virtual bool IsVisible() => FindBy.IsElementVisible();
    }
}
using OpenQA.Selenium;
using System;
using System.Globalization;

namespace AutomationFramework.Engine.Controls
{
    public class Text : IPageField<string>
    {
        public string Name { get; }
        public By FindBy { get; }

        public Text(string name, By findBy)
        {
            Name = name;
            FindBy = findBy;
        }

        public virtual string Get()
        {
            if (!IsVisible()) { throw new Exception($"Element {FindBy.ToString()} not found or is not visible"); };

            var element = FindBy.FindElement();

            return element.Text;
        }

        public virtual void Set(string value)
        {
            FindBy.WaitUntilElementIsVisible();
            FindBy.FindElement().Clear();
            FindBy.FindElement().SendKeys(value);
        }

        public virtual void Set(double value) => Set(value.ToString(CultureInfo.InvariantCulture));

        public virtual void Clear() => FindBy.FindElement().Clear();

        public virtual bool Exists() => FindBy.DoesElementExist();

        public virtual bool IsVisible() => FindBy.IsElementVisible();
    }
}

Our classes each have a constructor that takes two parameters. The name as a string, and a By object, which is our locator. The name is entirely optional and in there for readability, in theory you could eliminate this by enforcing strong naming standards with your variables. The By findBy is definitely required however, and will be used to find our element (using the ID, Xpath, CSS Selector etc).

As you can see, all of the classes above share the properties and methods defined in our interface, they have to do this as a minimum. This doesn’t mean they can’t include extra functionality though as shown in our picklist class. We have quite a few methods specific to that element type that we have implemented but aren’t available to element types.

To really appreciate why this extra effort is worth it as opposed to just declaring all our elements as IWebElements, we should probably look at how this is used in code. An example page object class using the above elements might look as follows:

using OpenQA.Selenium;
using AutomationFramework.Engine.Controls;

namespace AutomationFramework.Web.Tests.Pages
{
    public static class LoginPage
    {
        public static Button LoginButton = new Button();
        public static Button ForgotPassword = new Button();

        public static Text UsernameInput = new Text("UsernameInput", By.Id("username"));
        public static Text PasswordInput = new Text("PasswordInput", By.Id("password"));

        public static Picklist UserType = new Picklist("UserType", By.Id("usertype"));

        public static Checkbox TermsAndConditions = new Checkbox("TermsAndConditions", By.Id("tandc"));
    }
}

Here you can see that each new element we create on a page, we create an object of the element type we’re wanting to interact with. This makes your page object classes extremely easy to follow, as well as making them much easier to maintain when going back to after many months away, as straight away it’s clear to see what objects make up the page. Without doing it this way, you’d specify each element the following way:

public static IWebElement LoginButton = Driver.FindElement(By.Id("LoginButton"));

Looking at the LoginButton in that example compared to our Button object, you can see that from a readability perspective, it’s much better to use our Button object. However, it’s also a lot better from a usability perspective too. Going back to our picklist class, we specified methods that were unique to that element type, this means that only a picklist object can use them. If we tried calling them from the Button object, although they both inherit from the same interface, the button object doesn’t have access to those unique picklist methods. This prevents sloppy code or mistakes when trying to interact with an element in an incorrect way and allows you or your other users to create code with more confidence, from being able to know exactly how they can use each object type.

This brings us to the end of part four. Although this functionality we’ve added isn’t required in a framework, it has improved the way we will write tests and create page object classes. It also is good introduction to using interfaces if you’ve not seen those before.

Next time we’re going to look at adding some functionality to Selenium WebDriver, and create some extension methods.¬†Extension methods allow us to add methods to existing types without creating a new derived type, recompiling, or modify the original types. So in this example, adding to the existing WebDriver type, and more specifically in this case, improving how we find and wait on elements.