65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (6投票s)

2016年6月27日

Ms-PL

7分钟阅读

viewsIcon

17677

构建一个混合测试自动化框架。学习如何遵循 SOLID 原则为其创建抽象的 Selenium WebDriver 实现。文章“创建混合测试框架 – Selenium Driver 实现”首次发布于 Automate The Planet。

引言

这是新系列文章的第二篇 - 设计与架构。在该系列的第一篇出版物中,我向您展示了如何开始创建 混合测试自动化框架 - 创建混合框架的核心接口。在这篇文章中,我将向您展示如何通过 Selenium WebDriver 构建这些接口的第一个具体实现。此外,我将解释如何通过 Unity IoC 容器 解析特定的驱动程序和元素。

创建 Selenium Driver 实现

您的第一个任务是创建一个名为 HybridTestFramework.UITests.Selenium 的新项目。然后,引用先前创建的核心项目 HybridTestFramework.UITests.Core,该项目包含混合框架的接口。您还需要安装几个 NuGet 包 - Selenium.WebDriverUnity。下面是项目的样子。对于每个核心驱动接口,我们将创建一个新的 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 参数传递搜索上下文。IWebDriverIWebElement 都是实现了 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

大多数人可能不需要使用更高级的定位器,所以我将它们放在一个名为 AdvancedByBy 类的专用子类中。当然,您需要将新的定位器策略添加到 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);
}

测试主体没有任何变化。所有必要的更改都需要放在页面的元素映射中。

设计与架构

文章 创建混合测试框架 – 高级元素查找扩展 首次发布于 Automate The Planet

所有图片均从 DepositPhotos.com 购买,不得免费下载和使用。
许可协议

文章 创建混合测试框架 – Selenium Driver 实现 首次发布于 Automate The Planet

所有图片均从 DepositPhotos.com 购买,不得免费下载和使用。
许可协议

© . All rights reserved.