创建混合测试框架 – Selenium Driver 实现






4.95/5 (6投票s)
构建一个混合测试自动化框架。学习如何遵循 SOLID 原则为其创建抽象的 Selenium WebDriver 实现。文章“创建混合测试框架 – Selenium Driver 实现”首次发布于 Automate The Planet。
引言
这是新系列文章的第二篇 - 设计与架构。在该系列的第一篇出版物中,我向您展示了如何开始创建 混合测试自动化框架 - 创建混合框架的核心接口。在这篇文章中,我将向您展示如何通过 Selenium WebDriver 构建这些接口的第一个具体实现。此外,我将解释如何通过 Unity IoC 容器 解析特定的驱动程序和元素。
创建 Selenium Driver 实现
您的第一个任务是创建一个名为 HybridTestFramework.UITests.Selenium
的新项目。然后,引用先前创建的核心项目 HybridTestFramework.UITests.Core
,该项目包含混合框架的接口。您还需要安装几个 NuGet 包 - Selenium.WebDriver
和 Unity
。下面是项目的样子。对于每个核心驱动接口,我们将创建一个新的 SeleniumDriver partial
类。这些 partial
类将放置在 Engine 文件夹下。

实现 IDriver 接口
主类定义被声明为 partial
,并包含 SeleniumDriver
实现的构造函数。它需要 Unity IoC 容器的实例和 BrowserSettings
,其中包含不同的执行设置。根据配置的设置的浏览器类型,在 ResolveBrowser
方法中创建一个新的 WebDriver
实例。
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;
}
}
}
实现 IElementFinder 接口
如果您仔细查看解决方案的图片,您可能会注意到 Engine 文件夹下的大多数文件都带有前缀 ‘SeleniumDriver
’。它们都是 SeleniumDriver partial
类,但由于文件结构不同,文件名也不同。下面的代码放在 SeleniumDriver.ElementFinder
类中。您可以始终将所有代码放在一个文件中,但这样会使文件变得非常大。
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);
}
}
这些方法调用 ElementFinderService
的实现。最重要的一点是,当前的搜索上下文是 IWebDriver
(这意味着驱动程序将在整个页面中查找元素)。
ElementFinderService
创建专用类来进行元素定位的目的是,您可以一次搜索整个页面,但也可以使用不同的元素作为搜索上下文(例如 div
元素)。该类存在是因为我们不希望有代码重复。我们通过 ISearchContext
参数传递搜索上下文。IWebDriver
和 IWebElement
都是实现了 ISearchContext
的 Selenium 接口。您可以使用它们两者来搜索元素。
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;
}
}
代码中另一个有趣的部分是我们如何解析被搜索元素的类型。所有控件的具体 Selenium 实现都实现了 IElement
接口。在我们可以确定元素类型之前,我们需要在 Unity 容器中注册其类型。此外,由于所有元素都需要一些参数,因此我们通过 resolve override(即 Unity 在创建对象时注入传入的实例)来传递它们。在文章的最后,您将找到如何正确注册不同的类型。
实现 IElement 接口
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);
}
}
这个 IElement
接口的具体实现几乎没有什么特别之处,除了查找方法。这里搜索上下文是找到的元素本身。
实现 IBrowser 接口
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();
}
}
此类中的不同方法仅封装了 WebDriver
的原生方法。您可以在 我的高级技巧文章 中找到更多关于它们的信息。
实现 ICookieService 接口
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();
}
}
您可以在您的 页面 或测试中使用 ICookieService
接口来处理 cookie。WebDriver
提供了对 cookie 的全面支持。
实现 INavigationService 接口
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));
}
}
}
隐式等待和显式等待不应该在同一个测试中一起使用,因此在使用显式等待时,我们会删除隐式等待。另外,另一个有趣的部分是我们使用了 HttpUtility
类的 UrlDecode
方法来解码 URL。页面加载后,我们会触发 Navigated
事件,任何已订阅的代码都将被执行。例如,等待特定元素显示。
实现 IJavaScriptInvoker 接口
public partial class SeleniumDriver : IJavaScriptInvoker
{
public string InvokeScript(string script)
{
IJavaScriptExecutor javaScriptExecutor =
driver as IJavaScriptExecutor;
return (string)javaScriptExecutor.ExecuteScript(script);
}
}
您不需要将整个 IDriver
接口传递给您的页面,只需传递其中需要的部分。例如,如果您只需要在页面上执行 JavaScript 代码,那么您才会添加上面的实现。
测试中的 Selenium Driver 实现
注册类型和实例 - Unity IoC 容器
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>();
}
您可以使用配置文件注册所有类型。您可以在我的文章中阅读更多相关信息 - 使用 IoC 容器创建功能强大的 Page Object Pattern。通常,上面的代码应该放在您的 AssemblyInitialize
方法中,而不是 TestInitialize
中。您需要将更大的 IDriver
接口的所有部分映射到 Selenium Driver 实现。这样,如果您尝试解析其中一个较小的接口,就不会抛出异常。此外,您还需要注册 Unity 的创建实例,因为引擎的某些代码需要它。相同的规则也适用于设置。最后,您需要将所有控件类注册到容器中。在这里,我注册了 Element
和 ButtonControl
类(它们的 Selenium 实现)。
测试示例
[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);
}
引言
这是 设计与架构系列 的最新文章。在该系列的第一篇文章中,我向您展示了如何基于 abstract
类创建用于查找元素的通用接口。但是,我们可以将这个想法进一步扩展。在这里,我将向您展示如何为 ElementFinder
接口创建扩展,以便您可以编写更少的代码来定位元素,并使用更复杂的定位器。
混合测试框架 - 创建高级元素查找扩展
ElementFinder 的基本实现
下面是 IElementFinder
接口的基本实现。您可以通过 By
定位器进行配置,使用通用的 Find
方法来定位 Web 元素。但是,我认为这对于定位单个元素来说需要编写太多代码。我将向您展示如何创建不需要 By
配置甚至包含更复杂定位器的 Find
方法。
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);
}
}
基本 By
By
类的基本实现仅包含最重要的定位器,例如按 ID
、类名、CSS、链接文本和标签名查找。在我的测试中,大多数时候我使用更复杂的定位器策略,例如按 ID 结尾、ID 包含、XPath、XPath 包含等等。因此,我认为拥有这些工具会很有用。
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);
}
}
高级 By
大多数人可能不需要使用更高级的定位器,所以我将它们放在一个名为 AdvancedBy
的 By
类的专用子类中。当然,您需要将新的定位器策略添加到 SearchType
枚举中。
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 扩展 - AdvancedElementFinder
我决定增强基本 ElementFinder
的最佳方法是为它创建扩展方法。您可以进一步扩展这个想法,并将包含扩展方法的类放在一个专用项目中,这样只有当有人需要时才能添加引用。Selenium.WebDriver.Support
NuGet 包的工作方式相同。我们为每个新的高级定位策略创建一个附加方法。
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));
}
}
测试中的高级 Element Find 扩展
Page Object Map
为了能够使用新的高级定位器方法,您只需要向它们的命名空间添加一个 using
语句。之后,您就可以通过 Visual Studio 的 IntelliSense 在它们之间进行选择。
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"));
}
}
}
测试示例
[TestMethod]
public void SearchForAutomateThePlanet()
{
var bingMainPage = this.container.Resolve<BingMainPage>();
bingMainPage.Navigate();
bingMainPage.Search("Automate The Planet");
bingMainPage.AssertResultsCountIsAsExpected(264);
}
测试主体没有任何变化。所有必要的更改都需要放在页面的元素映射中。
设计与架构
- 创建混合测试框架 – 测试框架驱动程序控件 17/07/2016 / 作者 Anton Angelov
- 创建混合测试框架 – 测试框架驱动程序实现 10/07/2016 / 作者 Anton Angelov
- 创建混合测试框架 – Selenium Driver 控件 03/07/2016 / 作者 Anton Angelov
- 创建混合测试框架 – Selenium Driver 实现 26/06/2016 / 作者 Anton Angelov
- 创建混合测试自动化框架 – 接口契约 19/06/2016 / 作者 Anton Angelov
文章 创建混合测试框架 – 高级元素查找扩展 首次发布于 Automate The Planet。
所有图片均从 DepositPhotos.com 购买,不得免费下载和使用。
许可协议
文章 创建混合测试框架 – Selenium Driver 实现 首次发布于 Automate The Planet。
所有图片均从 DepositPhotos.com 购买,不得免费下载和使用。
许可协议