Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Create Hybrid Test Automation Framework – Interface Contracts

5.00/5 (2 votes)
19 Jun 2016Ms-PL6 min read 15.2K  
Build a Hybrid Test Automation Framework. Learn how to create an abstract Selenium WebDriver Implementation of it following SOLID principles.

Introduction

Guys, I am so excited to announce the creation of new series of blog posts- Design & Architecture. The main idea behind this is that I will show more abstract, visionary improvements that you might bring to your test automation projects that do not depend on the used test automation framework such as WebDriver or Testing Framework. The first articles from Design & Architecture Series are going to be dedicated to the creation of a Hybrid Test Automation Framework. Through this type of test automation framework, you can quickly execute your tests through different test automation frameworks without changing a single line of code, using only configuration switches.

As you may guess, the creation of a Hybrid Test Automation Framework is not an easy job. A lot of code should be written so I cannot explain everything in a single post. Because of that, I am going to separate logically in the best possible way the content. In this first article, I am going to explain to you how to create the core interface contracts that your test pages and tests will use so that they do not depend on a concrete implementation and at the same time, follow the best practices and SOLID principles.

Hybrid Test Automation Framework- Interfaces

Primary IDriver Interface

This is the main interface that you will use in your code. As you can find out from the lines below, it does not contain any methods, it only inherits a couple of other important contacts. The main idea behind this is to follow the Interface Segregation SOLID Principle. If you need to find elements, you will use the IElementFinder interface if you require updating cookies you will use the ICookieService and so forth. The principle states that no client should be forced to depend on methods that it does not use so we split the big interface in several smaller logically separated parts.

C#
public interface IDriver : 
    IElementFinder,
    INavigationService, 
    ICookieService, 
    IDialogService, 
    IJavaScriptInvoker,
    IBrowser
{
}

IElementFinder Interface

C#
public interface IElementFinder
{
    TElement Find<TElement>(By by) where TElement : class, IElement;

    IEnumerable<TElement> FindAll<TElement>(By by) where TElement : class, IElement;

    bool IsElementPresent(Core.By by);
}

The IElementFinder contract holds methods for locating elements on the pages. Also, it contains a logic for checking if an element is present.

The methods return IElement interface which represents a base HTML page element.

C#
public interface IElement : IElementFinder
{
    string GetAttribute(string name);

    void WaitForExists();

    void WaitForNotExists();

    void Click();

    void MouseClick();

    bool IsVisible { get; }

    int Width { get; }

    string CssClass { get; }

    string Content { get; }
}

To support searching of elements inside other container items such as DIVs, IElement inherits from the IElementFinder interface. All different controls will inherit from the IElement contract. You will find more information about this in the next articles from the series.

Similar to the WebDriver implementation, we have an abstract static class By for setting the elements' localization strategy.

C#
public class By
{
    public By(SearchType type, string value) : 
    this(type, value, null)
    {
    }

    public SearchType Type { get; private set; }

    public string Value { get; private set; }

    public static By Id(string id)
    {
        return new By(SearchType.Id, id);
    }

    public static By InnerTextContains(string innerText)
    {
        return new By(SearchType.InnerTextContains, innerText);
    }

    public static By Xpath(string xpath)
    {
        return new By(SearchType.XPath, xpath);
    }
  
    public static By Id(string id, IElement parentElement)
    {
        return new By(SearchType.Id, id, parentElement);
    }

    public static By CssClass(string cssClass)
    {
        return new By(SearchType.CssClass, cssClass);
    }
   
    public static By Name(string name)
    {
        return new By(SearchType.Name, name);
    }
}

IJavaScriptInvoker Interface

It holds a logic for JavaScript execution.

C#
public interface IJavaScriptInvoker
{
    string InvokeScript(string script);
}

INavigationService Interface

INavigationService interface has several methods regarding navigation such as navigating by a relative URL or absolute URL. Also, it contains logic for waiting for specific URL.

C#
public interface INavigationService
{
    event EventHandler<PageEventArgs> Navigated;

    string Url { get; }

    string Title { get; }

    void Navigate(string relativeUrl, string currentLocation, bool sslEnabled = false);

    void NavigateByAbsoluteUrl(string absoluteUrl, bool useDecodedUrl = true);

    void Navigate(string currentLocation, bool sslEnabled = false);

    void WaitForUrl(string url);

    void WaitForPartialUrl(string url);
}

IDialogService Interface

Through it, you can handle different dialogs.

C#
public interface IDialogService
{
    void Handle(System.Action action = null, DialogButton dialogButton = DialogButton.OK);

    void HandleLogonDialog(string userName, string password);

    void Upload(string filePath);
}

IBrowser Interface

This is one of the most important interfaces, part of the main IDriver interface. Through the IBrowser contract, you can execute browser-specific actions such as switching frames, refreshing, clicking back/forward buttons and so on.

C#
public interface IBrowser
{
    BrowserSettings BrowserSettings { get; }
        
    string SourceString { get; }

    void SwitchToFrame(IFrame newContainer);

    IFrame GetFrameByName(string frameName);

    void SwitchToDefault();

    void Quit();

    void WaitForAjax();

    void WaitUntilReady();

    void FullWaitUntilReady();

    void RefreshDomTree();

    void ClickBackButton();

    void ClickForwardButton();

    void LaunchNewBrowser();

    void MaximizeBrowserWindow();

    void ClickRefresh();
}

Hybrid Test Automation Framework in Tests

This is how the base page for all pages of your hybrid test automation framework will look like.

Hybrid Test Automation Framework Base Page

C#
public abstract class BasePage
{
    private readonly IElementFinder elementFinder;
    private readonly INavigationService navigationService;

    public BasePage(IElementFinder elementFinder, INavigationService navigationService)
    {
        this.elementFinder = elementFinder;
        this.navigationService = navigationService;
    }

    protected IElementFinder ElementFinder
    {
        get
        {
            return this.elementFinder;
        }
    }

    protected INavigationService NavigationService
    {
        get
        {
            return this.navigationService;
        }
    }
}

As you can see, the base page does not require all interfaces of the IDriver contract. Most pages need only a way to find elements and to navigate.

Non-Hybrid Base Page

To see the difference, you can find below the code of the non-hybrid version of the BasePage class.

C#
public abstract class BasePage
{
    protected IWebDriver driver;

    public BasePage(IWebDriver driver)
    {
        this.driver = driver;
    }

    public abstract string Url { get; }

    public virtual void Open(string part = "")
    {
        this.driver.Navigate().GoToUrl(string.Concat(this.Url, part));
    }
}

As you can see, we pass the whole IWebDriver interface. However, often we do not need all methods that it exposes.

Hybrid Page Object

C#
public partial class BingMainPage : BasePage
{
    public BingMainPage(
        IElementFinder elementFinder, 
        INavigationService navigationService)
        : base(elementFinder, navigationService)
    {
    }

    public void Navigate()
    {
        this.NavigationService.NavigateByAbsoluteUrl(@"http://www.bing.com/");
    }

    public void Search(string textToType)
    {
        // It is going to be implemented in the next article.
        ////this.SearchBox.Clear();
        ////this.SearchBox.SendKeys(textToType);
        this.GoButton.Click();
    }
    
    public int GetResultsCount()
    {
        int resultsCount = default(int);
        resultsCount = int.Parse(this.ResultsCountDiv.Content);
        return resultsCount;
    }

Similar to the base page, here we pass only the abstract hybrid test automation framework's contracts. Also, we need to implement the Navigate method manually.

Non-Hybrid Page Object

C#
public partial class BingMainPage : BasePage
{
    public BingMainPage(IWebDriver driver) : base(driver)
    {
    }

    public override string Url
    {
        get
        {
            return @"http://www.bing.com/";
        }
    }

    public void Search(string textToType)
    {
        this.SearchBox.Clear();
        this.SearchBox.SendKeys(textToType);
        this.GoButton.Click();
    }
    
    public int GetResultsCount()
    {
        int resultsCount = default(int);
        resultsCount = int.Parse(this.ResultsCountDiv.Text);
        return resultsCount;
    }
}

The only difference compared to the hybrid version is that the BingMainPage is coupled with the concrete IWebDriver implementation.

Hybrid Page Map

C#
public partial class BingMainPage
{
    public IElement SearchBox
    {
        get
        {
            return this.ElementFinder.Find<IElement>(By.Id("sb_form_q"));
        }
    }

    public IElement GoButton
    {
        get
        {
            return this.ElementFinder.Find<IElement>(By.Id("sb_form_go"));
        }
    }

    public IElement ResultsCountDiv
    {
        get
        {
            return this.ElementFinder.Find<IElement>(By.Id("b_tween"));
        }
    }
}

Here, I used the improved version of the Page Object Pattern - the element map is implemented as a partial class of the primary page object class. We use the ElementFinder property that comes from the BasePage class to locate the different elements. As you have probably noticed, the different properties return the IElement interface so that the map is not coupled with the concrete implementation of the controls.

Non-Hybrid Page Map

C#
public partial class BingMainPage : BasePage
{
    public IWebElement SearchBox
    {
        get
        {
            return this.driver.FindElement(By.Id("sb_form_q"));
        }
    }

    public IWebElement GoButton
    {
        get
        {
            return this.driver.FindElement(By.Id("sb_form_go"));
        }
    }

    public IWebElement ResultsCountDiv
    {
        get
        {
            return this.driver.FindElement(By.Id("b_tween"));
        }
    }
}

Similar to the page object class, the non-hybrid element map is coupled with the WebDriver's concrete implementation.

Hybrid Test Automation Framework Test Example

C#
[TestClass]
public class BingTests
{
    private BingMainPage bingMainPage;
    private IDriver driver;

    [TestInitialize]
    public void SetupTest()
    {
        this.driver = new SeleniumDriver();
        this.bingMainPage = new BingMainPage(this.driver, this.driver);
    }

    [TestCleanup]
    public void TeardownTest()
    {
        this.driver.Quit();
    }

    [TestMethod]
    public void SearchForAutomateThePlanet()
    {
        this.bingMainPage.Navigate();
        this.bingMainPage.Search("Automate The Planet");
        this.bingMainPage.AssertResultsCountIsAsExpected(264);
    }
}

As you can see from both examples, the code is almost identical with the only difference that our test automation framework can be switched in the first version if you assign another concrete implementation to the IDriver interface variable.

Non-Hybrid Test Automation Framework Test Example

C#
[TestClass]
public class BingTests
{
    private BingMainPage bingMainPage;
    private IWebDriver driver;

    [TestInitialize]
    public void SetupTest()
    {
        this.driver = new FirefoxDriver();
        this.bingMainPage = new BingMainPage(this.driver);
    }

    [TestCleanup]
    public void TeardownTest()
    {
        this.driver.Quit();
    }

    [TestMethod]
    public void SearchForAutomateThePlanet()
    {
        this.bingMainPage.Open();
        this.bingMainPage.Search("Automate The Planet");
        this.bingMainPage.AssertResultsCountIsAsExpected(264);
    }
}

This is the newest article from the Design & Architecture Series. In the first articles from the series, I showed you how to create a common interface for finding elements based on abstract classes. However, we can extend the idea even further. Here, I am going to show you how to create extensions for the ElementFinder interface so that you can locate elements with less writing and with more complex locators.

Hybrid Test Framework- Create Advanced Element - Find Extensions

Basic Implementation of ElementFinder

Below, you can find the basic implementation of the IElementFinder interface. You can locate web elements using the generic Find method through configuring it via By locator. However, I think that it requires too much writing for locating a single element. I will show you how to create Find methods that do not require By configuration and even contains more advanced locators.

C#
public partial class SeleniumDriver : IElementFinder
{
    public TElement Find<TElement>(Core.By by) 
    where TElement : class, Core.Controls.IElement
    {
        return this.elementFinderService.Find<TElement>(this.driver, by);
    }

    public IEnumerable<TElement> FindAll<TElement>(Core.By by) 
    where TElement : class, Core.Controls.IElement
    {
        return this.elementFinderService.FindAll<TElement>(this.driver, by);
    }

    public bool IsElementPresent(Core.By by)
    {
        return this.elementFinderService.IsElementPresent(this.driver, by);
    }
}

Basic By

The basic implementation of the By class includes just the most important locators such as find by ID, class, CSS, link text and tag name. Most of the time, in my tests, I use more complicated locators' strategies such as find by ID ending with, ID containing, XPath, XPath contains and so on. So I believe it is useful to have these at your disposal.

C#
public class By
{
    public By(SearchType type, string value) : this(type, value, null)
    {
    }

    public By(SearchType type, string value, IElement parent)
    {
        this.Type = type;
        this.Value = value;
        this.Parent = parent;
    }

    public SearchType Type { get; private set; }

    public string Value { get; private set; }

    public IElement Parent { get; private set; }

    public static By Id(string id)
    {
        return new By(SearchType.Id, id);
    }
        
    public static By Id(string id, IElement parentElement)
    {
        return new By(SearchType.Id, id, parentElement);
    }

    public static By LinkText(string linkText)
    {
        return new By(SearchType.LinkText, linkText);
    }

    public static By CssClass(string cssClass, IElement parentElement)
    {
        return new By(SearchType.CssClass, cssClass, parentElement);
    }

    public static By Tag(string tagName)
    {
        return new By(SearchType.Tag, tagName);
    }

    public static By Tag(string tagName, IElement parentElement)
    {
        return new By(SearchType.Tag, tagName, parentElement);
    }

    public static By CssSelector(string cssSelector)
    {
        return new By(SearchType.CssSelector, cssSelector);
    }

    public static By CssSelector(string cssSelector, IElement parentElement)
    {
        return new By(SearchType.CssSelector, cssSelector, parentElement);
    }

    public static By Name(string name)
    {
        return new By(SearchType.Name, name);
    }

    public static By Name(string name, IElement parentElement)
    {
        return new By(SearchType.Name, name, parentElement);
    }
}

Advanced By

Most of the people may not need to use the more advanced locators, so I put them in a dedicated child of the By class, named AdvancedBy. Of course, you need to add the new locators' strategies in the SearchType enum.

C#
public class AdvancedBy : By
{
    public AdvancedBy(SearchType type, string value, IElement parent) 
        : base(type, value, parent)
    {
    }

    public static By IdEndingWith(string id)
    {
        return new By(SearchType.IdEndingWith, id);
    }

    public static By ValueEndingWith(string valueEndingWith)
    {
        return new By(SearchType.ValueEndingWith, valueEndingWith);
    }

    public static By Xpath(string xpath)
    {
        return new By(SearchType.XPath, xpath);
    }

    public static By LinkTextContaining(string linkTextContaing)
    {
        return new By(SearchType.LinkTextContaining, linkTextContaing);
    }

    public static By CssClass(string cssClass)
    {
        return new By(SearchType.CssClass, cssClass);
    }

    public static By CssClassContaining(string cssClassContaining)
    {
        return new By(SearchType.CssClassContaining, cssClassContaining);
    }

    public static By InnerTextContains(string innerText)
    {
        return new By(SearchType.InnerTextContains, innerText);
    }

    public static By NameEndingWith(string name)
    {
        return new By(SearchType.NameEndingWith, name);
    }

    public static By XPathContaining(string xpath)
    {
        return new By(SearchType.XPathContaining, xpath);
    }

    public static By IdContaining(string id)
    {
        return new By(SearchType.IdContaining, id);
    }
}

ElementFinder Extensions- AdvancedElementFinder

I decided that the best way to enhance the basic ElementFinder is to create extension methods for it. You can extend the idea even further and place the class with extension methods in a dedicated project so that only if someone needs them to add a reference. The Selenium.WebDriver.Support NuGet works in the same manner. We create an additional method for each new advanced localization strategy.

C#
public static class AdvancedElementFinder
{
    public static TElement FindByIdEndingWith<TElement>(
        this IElementFinder finder, string idEnding) 
        where TElement : class, IElement
    {
        return finder.Find<TElement>(AdvancedBy.IdEndingWith(idEnding));
    }

    public static TElement FindByIdContaining<TElement>(
        this IElementFinder finder, string idContaining) 
        where TElement : class, IElement
    {
        return finder.Find<TElement>(AdvancedBy.IdContaining(idContaining));
    }

    public static TElement FindByValueEndingWith<TElement>(
        this IElementFinder finder, string valueEnding) 
        where TElement : class, IElement
    {
        return finder.Find<TElement>(AdvancedBy.ValueEndingWith(valueEnding));
    }

    public static TElement FindByXpath<TElement>(
        this IElementFinder finder, string xpath) 
        where TElement : class, IElement
    {
        return finder.Find<TElement>(AdvancedBy.Xpath(xpath));
    }

    public static TElement FindByLinkTextContaining<TElement>(
        this IElementFinder finder, string linkTextContaining) 
        where TElement : class, IElement
    {
        return finder.Find<TElement>(AdvancedBy.LinkTextContaining(linkTextContaining));
    }

    public static TElement FindByClass<TElement>(
        this IElementFinder finder, string cssClass) 
        where TElement : class, IElement
    {
        return finder.Find<TElement>(AdvancedBy.CssClass(cssClass));
    }

    public static TElement FindByClassContaining<TElement>(
        this IElementFinder finder, string cssClassContaining) 
        where TElement : class, IElement
    {
        return finder.Find<TElement>(AdvancedBy.CssClassContaining(cssClassContaining));
    }

    public static TElement FindByInnerTextContaining<TElement>(
        this IElementFinder finder, string innerText) 
        where TElement : class, IElement
    {
        return finder.Find<TElement>(AdvancedBy.InnerTextContains(innerText));
    }

    public static TElement FindByNameEndingWith<TElement>(
        this IElementFinder finder, string name) 
        where TElement : class, IElement
    {
        return finder.Find<TElement>(AdvancedBy.NameEndingWith(name));
    }
}

Advanced Element Find Extensions in Tests

Page Object Map

In order to be able to use the new advanced locators' methods, you only need to add a using statement to their namespace. After that, you will be able to choose between them through the Visual Studio's IntelliSence.

C#
public partial class BingMainPage
{
    public ITextBox SearchBox
    {
        get
        {
            ////return this.ElementFinder.Find<ITextBox>(By.Id("sb_form_q"));
            return this.ElementFinder.FindByIdEndingWith<ITextBox>("sb_form_q");
        }
    }

    public IButton GoButton
    {
        get
        {
            return this.ElementFinder.Find<IButton>(By.Id("sb_form_go"));
        }
    }

    public IDiv ResultsCountDiv
    {
        get
        {
            return this.ElementFinder.Find<IDiv>(By.Id("b_tween"));
        }
    }
}

Test Example

C#
[TestMethod]
public void SearchForAutomateThePlanet()
{
    var bingMainPage = this.container.Resolve<BingMainPage>();
    bingMainPage.Navigate();
    bingMainPage.Search("Automate The Planet");
    bingMainPage.AssertResultsCountIsAsExpected(264);
}

There are no changes in the tests' bodies. All necessary changes need to be placed in the pages' element maps.

Design & Architecture

The post Create Hybrid Test Automation Framework – Interface Contracts appeared first on Automate The Planet.

All images are purchased from DepositPhotos.com and cannot be downloaded and used for free.
License Agreement

License

This article, along with any associated source code and files, is licensed under The Microsoft Public License (Ms-PL)