Click here to Skip to main content
15,867,686 members
Articles / Programming Languages / C#

Create Hybrid Test Framework – Selenium Driver Implementation

Rate me:
Please Sign up or sign in to vote.
4.95/5 (6 votes)
26 Jun 2016Ms-PL7 min read 16.5K   5   1
Build a Hybrid Test Automation Framework. Learn how to create an abstract Selenium WebDriver Implementation of it following SOLID principles.The post Create Hybrid Test Framework – Selenium Driver Implementation appeared first on Automate The Planet.

Introduction

This is the second article from the new series- Design & Architecture. In the first publication from the series, I showed you how to start creating a Hybrid Test Automation Framework- creating core interfaces of the hybrid framework. In this post, I am going to show you how to build your first concrete implementation of these interfaces through Selenium WebDriver. Also, I am going to explain how to resolve the particular drivers and elements through Unity IoC Container.

Create Selenium Driver Implementation

The first job you have is to create a new project named HybridTestFramework.UITests.Selenium. After that, reference the previously created core project HybridTestFramework.UITests.Core that holds the hybrid framework's interfaces. You also need to install a couple of NuGets - Selenium.WebDriver and Unity. Below, you can find how the projects should look like. For every core driver interface, we are going to create a new SeleniumDriver partial class. These partial classes are going to be placed under the Engine folder.

Image 1

Implement IDriver Interface

The main class definition is declared as partial and contains the constructor of the SeleniumDriver implementation. It requires an instance of the Unity IoC Container and BrowserSettings where different execution settings are placed. Based on the configured settings' browser type, a new WebDriver instance is created in the ResolveBrowser method.

C#
public partial class SeleniumDriver : IDriver
{
    private IWebDriver driver;
    private IUnityContainer container;
    private BrowserSettings browserSettings;
    private readonly ElementFinderService elementFinderService;

    public SeleniumDriver(IUnityContainer container, BrowserSettings browserSettings)
    {
        this.container = container;
        this.browserSettings = browserSettings;
        this.ResolveBrowser(browserSettings);
        this.elementFinderService = new ElementFinderService(container);
        driver.Manage().Timeouts().ImplicitlyWait(
            TimeSpan.FromSeconds(browserSettings.ElementsWaitTimeout));
    }

    private void ResolveBrowser(BrowserSettings browserSettings)
    {
        switch (browserSettings.Type)
        {
            case Browsers.NotSet:
                break;
            case Browsers.Chrome:
                break;
            case Browsers.Firefox:
                this.driver = new FirefoxDriver();
                break;
            case Browsers.InternetExplorer:
                break;
            case Browsers.Safari:
                break;
            case Browsers.NoBrowser:
                break;
            default:
                break;
        }
    }       
}

Implement IElementFinder Interface

If you look closely at the solution's image, you will probably notice that most of the files under the Engine folder have the prefix 'SeleniumDriver.'. All of them are SeleniumDriver partial classes, but the file names are different because of the file structure. The code below is placed in the SeleniumDriver.ElementFinder class. You can always place all of the code in a single file but it will have an enormous size.

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);
    }
}

The methods call the ElementFinderService's implementations. The most important part is that the current search context is the IWebDriver (meaning that the driver will look for the elements in the whole page).

ElementFinderService

The point of creating a dedicated class for element localization is that you can once search in the whole page, but you can also use the different elements as a search context (div elements). The class exists because we do not want to have a code duplication. We pass the search context through the ISearchContext parameter. IWebDriver and IWebElement are both Selenium interfaces that implement the ISearchContext. You can use them both to search elements.

C#
public class ElementFinderService
{
    private readonly IUnityContainer container;

    public ElementFinderService(IUnityContainer container)
    {
        this.container = container;
    }

    public TElement Find<TElement>(ISearchContext searchContext, Core.By by) 
        where TElement : class, Core.Controls.IElement
    {
        var element = searchContext.FindElement(by.ToSeleniumBy());
        TElement result = this.ResolveElement<TElement>(searchContext, element);

        return result;
    }

    public IEnumerable<TElement> FindAll<TElement>(ISearchContext searchContext, Core.By by) 
        where TElement : class, Core.Controls.IElement
    {
        var elements = searchContext.FindElements(by.ToSeleniumBy());
        List<TElement> resolvedElements = new List<TElement>();
        foreach (var currentElement in elements)
        {
            TElement result = this.ResolveElement<TElement>(searchContext, currentElement);
            resolvedElements.Add(result);
        }

        return resolvedElements;
    }

    public bool IsElementPresent(ISearchContext searchContext, Core.By by)
    {
        var element = this.Find<Element>(searchContext, by);
        return element.IsVisible;
    }

    private TElement ResolveElement<TElement>(
        ISearchContext searchContext,
    IWebElement currentElement)
        where TElement : class, Core.Controls.IElement
    {
        TElement result = this.container.Resolve<TElement>(new ResolverOverride[]
        {
            new ParameterOverride("driver", searchContext),
            new ParameterOverride("webElement", currentElement),
            new ParameterOverride("container", this.container)
        });
        return result;
    }
}

Another interesting part of the code is how we resolve the type of the searched element. All concrete Selenium implementations of the controls implement the IElement interface. Before we can determine the type of the element, we need to register its type in the Unity container. Also, because all elements require a couple of parameters, we pass them through resolve override (meaning that the Unity will inject the passed instances during the creation of the object). At the end of the article, you will find how to register the different types correctly.

Implement IElement Interface

C#
public class Element : IElement
{
    protected readonly IWebElement webElement;
    protected readonly IWebDriver driver;
    protected readonly ElementFinderService elementFinderService;

    public Element(IWebDriver driver, IWebElement webElement, IUnityContainer container)
    {
        this.driver = driver;
        this.webElement = webElement;
        this.elementFinderService = new ElementFinderService(container);
    }

    public string GetAttribute(string name)
    {
        return this.webElement.GetAttribute(name);
    }

    public void WaitForExists()
    {
        throw new NotImplementedException();
    }

    public void WaitForNotExists()
    {
        throw new NotImplementedException();
    }

    public void Click()
    {
        this.webElement.Click();
    }

    public void MouseClick()
    {
        Actions builder = new Actions(this.driver);
        builder.MoveToElement(this.webElement).Click().Build().Perform();
    }

    public bool IsVisible
    {
        get
        {
            return this.webElement.Displayed;
        }
    }

    public int Width
    {
        get
        {
            return this.webElement.Size.Width;
        }
    }

    public string CssClass
    {
        get
        {
            return webElement.GetAttribute("className");
        }
    }

    public string Content
    {
        get
        {
            return this.webElement.Text;
        }
    }

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

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

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

There is almost nothing special about this concrete implementation of the IElement interface, except the find methods. Here the search context is the found element itself.

Implement IBrowser Interface

C#
public partial class SeleniumDriver : IBrowser
{
    public BrowserSettings BrowserSettings
    {
        get
        {
            return this.browserSettings;
        }
    }

    public string SourceString
    {
        get
        {
            return this.driver.PageSource;
        }
    }

    public void SwitchToFrame(IFrame newContainer)
    {
        driver.SwitchTo().Frame(newContainer.Name);
    }

    public IFrame GetFrameByName(string frameName)
    {
        return new SeleniumFrame(frameName);
    }

    public void SwitchToDefault()
    {
        this.driver.SwitchTo().DefaultContent();
    }

    public void Quit()
    {
        this.driver.Quit();
    }

    public void ClickBackButton()
    {
        this.driver.Navigate().Back();
    }

    public void ClickForwardButton()
    {
        this.driver.Navigate().Forward();
    }

    public void MaximizeBrowserWindow()
    {
        driver.Manage().Window.Maximize();
    }

    public void ClickRefresh()
    {
        driver.Navigate().Refresh();
    }
}

The different methods in this class only wrap the WebDriver's native methods. You can find more about them in my advanced tips articles.

Implement ICookieService Interface

C#
public partial class SeleniumDriver : ICookieService
{
    public string GetCookie(string host, string cookieName)
    {
        var myCookie = this.driver.Manage().Cookies.GetCookieNamed(cookieName);
        return myCookie.Value;
    }

    public void AddCookie(string cookieName, string cookieValue, string host)
    {
        Cookie cookie = new Cookie(cookieName, cookieValue);
        this.driver.Manage().Cookies.AddCookie(cookie);
    }

    public void DeleteCookie(string cookieName)
    {
        this.driver.Manage().Cookies.DeleteCookieNamed("CookieName");
    }

    public void CleanAllCookies()
    {
        this.driver.Manage().Cookies.DeleteAllCookies();
    }
}

You can use the ICookieService interface in your pages or tests to work with cookies. WebDriver provides full cookies' support.

Implement INavigationService Interface

C#
public partial class SeleniumDriver : INavigationService
{
    public event EventHandler<Core.Events.PageEventArgs> Navigated;

    public string Url
    {
        get
        {
            return this.driver.Url;
        }
    }

    public string Title
    {
        get
        {
            return this.driver.Title;
        }
    }

    public void NavigateByAbsoluteUrl(
        string absoluteUrl, 
        bool useDecodedUrl = true)
    {
        var urlToNavigateTo = absoluteUrl;
        if (useDecodedUrl)
        {
            urlToNavigateTo = HttpUtility.UrlDecode(urlToNavigateTo);
        }
        this.driver.Navigate().GoToUrl(urlToNavigateTo);
    }

    public void WaitForUrl(string url)
    {
        this.driver.Manage().Timeouts().ImplicitlyWait(TimeSpan.FromSeconds(0));
        WebDriverWait wait = new WebDriverWait(
            this.driver, 
            TimeSpan.FromSeconds(this.browserSettings.ScriptTimeout));
        wait.PollingInterval = TimeSpan.FromSeconds(0.8);
        wait.Until(x => 
            string.Compare(x.Url, url, StringComparison.InvariantCultureIgnoreCase) == 0);
        this.RaiseNavigated(this.driver.Url);
        this.driver.Manage().Timeouts().ImplicitlyWait(TimeSpan.FromSeconds(3)); 
    }

    public void WaitForPartialUrl(string url)
    {
        this.driver.Manage().Timeouts().ImplicitlyWait(TimeSpan.FromSeconds(0));
        WebDriverWait wait = new WebDriverWait(
            this.driver, 
            TimeSpan.FromSeconds(this.browserSettings.ScriptTimeout));
        wait.PollingInterval = TimeSpan.FromSeconds(0.8);
        wait.Until(x => x.Url.Contains(url) == true);
        this.RaiseNavigated(this.driver.Url);
        this.driver.Manage().Timeouts().ImplicitlyWait(TimeSpan.FromSeconds(3)); 
    }

    private void RaiseNavigated(string url)
    {
        if (this.Navigated != null)
        {
            this.Navigated(this, new PageEventArgs(url));
        }
    }
}

Implicit and explicit waits should not be used in a test together that is why when using explicit, we are removing the implicit. Also, another interesting part is that we are using the UrlDecode method of the HttpUtility class to decode URLs. After the page is loaded, we rise the Navigated event, any subscribed code will be executed. For example, wait for a particular element to be displayed.

Implement IJavaScriptInvoker Interface

C#
public partial class SeleniumDriver : IJavaScriptInvoker
{
    public string InvokeScript(string script)
    {
        IJavaScriptExecutor javaScriptExecutor = 
            driver as IJavaScriptExecutor;
        return (string)javaScriptExecutor.ExecuteScript(script);
    }
}

You do not need to pass the whole IDriver interface to your pages, only the required parts of it. For example, if you need to execute a JavaScript code on the page, only then you will add the upper implementation.

Selenium Driver Implementation in Tests

Register Types and Instances- Unity IoC Container

C#
private IDriver driver;
private IUnityContainer container;

[TestInitialize]
public void SetupTest()
{
    this.container = new UnityContainer();
    this.container.RegisterType<IDriver, SeleniumDriver>();
    this.container.RegisterType<INavigationService, SeleniumDriver>();
    this.container.RegisterType<IBrowser, SeleniumDriver>();
    this.container.RegisterType<ICookieService, SeleniumDriver>();
    this.container.RegisterType<IDialogService, SeleniumDriver>();
    this.container.RegisterType<IElementFinder, SeleniumDriver>();
    this.container.RegisterType<IJavaScriptInvoker, SeleniumDriver>();
    this.container.RegisterType<IElement, Element>();
    this.container.RegisterType<IButton, ButtonControl>();
    this.container.RegisterInstance<IUnityContainer>(this.container);
    this.container.RegisterInstance<BrowserSettings>(BrowserSettings.DefaultFirefoxSettings);
    this.driver = this.container.Resolve<IDriver>();
}

You can register all types using a configuration file. You can read more about it in my article- Use IoC Container to Create Page Object Pattern on Steroids. Usually, the above code should be placed in your AssemblyInitialize method, not in the TestInitialize. You need to map all parts of the bigger IDriver interface to the Selenium Driver implementation. So that if you try to resolve some of the smaller interfaces, no exceptions to be thrown. Also, you need to register the created instance of the Unity because some of the engine's code requires it. The same is valid for the settings. Lastly, you need to register all of the controls' classes in the container. Here, I registered the Element and ButtonControl classes (their Selenium implementation).

Test Example

C#
[TestMethod]
public void NavigateToAutomateThePlanet()
{
    this.driver.NavigateByAbsoluteUrl(
@"http://automatetheplanet.com/");
    var blogButton = this.driver.Find<IButton>(
        By.Xpath("//*[@id='tve_editor']/div[2]/div[4]/div/div/div/div/div/a"));
    blogButton.Hover();
    Console.WriteLine(blogButton.Content);
    this.driver.NavigateByAbsoluteUrl(
@"http://automatetheplanet.com/download-source-code/");
    this.driver.ClickBackButton();
    Console.WriteLine(this.driver.Title);
}

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 Framework – Advanced Element Find Extensions appeared first on Automate The Planet.

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

The post Create Hybrid Test Framework – Selenium Driver Implementation 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)


Written By
CEO Automate The Planet
Bulgaria Bulgaria
CTO and Co-founder of Automate The Planet Ltd, inventor of BELLATRIX Test Automation Framework, author of "Design Patterns for High-Quality Automated Tests: High-Quality Test Attributes and Best Practices" in C# and Java. Nowadays, he leads a team of passionate engineers helping companies succeed with their test automation. Additionally, he consults companies and leads automated testing trainings, writes books, and gives conference talks. You can find him on LinkedIn every day.

Comments and Discussions

 
Questionyou should post the source not a link to your site Pin
shiftwik23-Dec-16 15:55
shiftwik23-Dec-16 15:55 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.