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

创建混合测试自动化框架——接口契约

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2016 年 6 月 20 日

Ms-PL

6分钟阅读

viewsIcon

15549

构建一个混合测试自动化框架。了解如何根据 SOLID 原则创建其抽象 Selenium WebDriver 实现。

引言

伙计们,我非常高兴地宣布推出一个新的博客文章系列——设计与架构。其主要思想是,我将展示您可以为测试自动化项目带来的更抽象、更具前瞻性的改进,这些改进不依赖于所使用的测试自动化框架,例如 WebDriver 或测试框架。设计与架构系列的第一篇文章将致力于创建混合测试自动化框架。通过这种类型的测试自动化框架,您可以通过不同的测试自动化框架快速执行测试,而无需更改一行代码,只需使用配置开关即可。

正如您可能猜测的,创建混合测试自动化框架并非易事。需要编写大量代码,因此我无法在单个帖子中解释所有内容。因此,我将以最佳方式在逻辑上分离内容。在这第一篇文章中,我将向您解释如何创建核心接口契约,您的测试页面和测试将使用这些契约,以便它们不依赖于具体的实现,同时遵循最佳实践和SOLID 原则

混合测试自动化框架——接口

主要 IDriver 接口

这是您将在代码中使用的主要接口。正如您从下面的行中可以发现的,它不包含任何方法,它只继承了几个其他重要的契约。这背后的主要思想是遵循接口隔离 SOLID 原则。如果您需要查找元素,您将使用IElementFinder接口;如果您需要更新 cookie,您将使用ICookieService等等。该原则指出,任何客户端都不应被迫依赖它不使用的方法,因此我们将大接口分成几个更小的逻辑上分离的部分。

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

IElementFinder 接口

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

IElementFinder契约包含用于在页面上定位元素的方法。此外,它还包含用于检查元素是否存在的逻辑。

这些方法返回IElement接口,该接口表示一个基本的 HTML 页面元素。

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

为了支持在其他容器项(如DIV)中搜索元素,IElement继承自IElementFinder接口。所有不同的控件都将继承自IElement契约。您将在本系列的后续文章中找到更多相关信息。

与 WebDriver 实现类似,我们有一个abstract staticBy,用于设置元素的定位策略。

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 接口

它包含 JavaScript 执行的逻辑。

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

INavigationService 接口

INavigationService接口有几个关于导航的方法,例如通过相对 URL 或绝对 URL 导航。此外,它还包含等待特定 URL 的逻辑。

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 接口

通过它,您可以处理不同的对话框。

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

    void HandleLogonDialog(string userName, string password);

    void Upload(string filePath);
}

IBrowser 接口

这是最重要的接口之一,是主要IDriver接口的一部分。通过IBrowser契约,您可以执行浏览器特定的操作,例如切换框架、刷新、点击后退/前进按钮等等。

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

混合测试自动化框架在测试中

这将是您的混合测试自动化框架所有页面的基页外观。

混合测试自动化框架基页

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

如您所见,基页不需要IDriver契约的所有接口。大多数页面只需要一种查找元素和导航的方法。

非混合基页

为了查看差异,您可以在下面找到非混合版本的BasePage类的代码。

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

如您所见,我们传递了整个IWebDriver接口。然而,我们通常不需要它公开的所有方法。

混合页面对象

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

与基页类似,这里我们只传递抽象混合测试自动化框架的契约。此外,我们还需要手动实现Navigate方法。

非混合页面对象

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

与混合版本的唯一区别是BingMainPage与具体的IWebDriver实现耦合。

混合页面映射

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

这里,我使用了页面对象模式的改进版本——元素映射被实现为主要页面对象类的部分类。我们使用来自BasePage类的ElementFinder属性来定位不同的元素。正如您可能已经注意到的,不同的属性返回IElement接口,因此映射不与控件的具体实现耦合。

非混合页面映射

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

与页面对象类类似,非混合元素映射与WebDriver的具体实现耦合。

混合测试自动化框架测试示例

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

从这两个示例中可以看出,代码几乎相同,唯一的区别是如果将另一个具体实现分配给IDriver接口变量,则在第一个版本中可以切换我们的测试自动化框架。

非混合测试自动化框架测试示例

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

这是设计与架构系列的最新文章。在该系列的第一篇文章中,我向您展示了如何基于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

大多数人可能不需要使用更高级的定位器,所以我将它们放在By类的一个专用子类中,名为AdvancedBy。当然,您需要在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 扩展 - AdvancedElementFinder

我决定增强基本ElementFinder的最佳方法是为其创建扩展方法。您可以进一步扩展这个想法,并将包含扩展方法的类放在一个专用项目中,这样只有当有人需要它们时才添加引用。Selenium.WebDriver.SupportNuGet 的工作方式相同。我们为每个新的高级本地化策略创建一个额外的方法。

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

测试中的高级元素查找扩展

页面对象映射

为了能够使用新的高级定位器方法,您只需要向其命名空间添加一个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 购买,不可免费下载和使用。
许可协议

© . All rights reserved.