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.
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.
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.
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.
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
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
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
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
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
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
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
[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.
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.
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
.
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.
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.
public partial class BingMainPage
{
public ITextBox SearchBox
{
get
{
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
[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
CodeProject
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