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

使代码更易于维护的页面对象

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2015年10月7日

Ms-PL

7分钟阅读

viewsIcon

19008

通过接口、扩展方法和部分类,展示了三种页面对象变体,它们可以实现更具可维护性的代码。

引言

如果您读过我之前的一些文章,您很可能看过我关于自动化测试中的设计模式的一些文章。我写过的最突出的模式之一是页面对象模式。它对于创建快速、健壮且可读的 UI 自动化至关重要,以至于我甚至专门为其撰写了第二篇文章。在我的团队中,我们已经在不同的项目中应用了它几年。在此过程中,我们提出了一些页面对象的不同变体。我认为其中一些变体比之前介绍的变体能带来更具可读性和可维护性的代码。

页面对象 v.2.0 – 接口

我非常喜欢这个模式,甚至为页面对象创建了一个流畅的 API。然而,我认为它的使用有点麻烦,至少对我来说是这样。事实证明,我并不是流畅接口的忠实拥趸。如果您读过我关于这个主题的文章,您可能会发现我建议将页面用作单例。尽管如此,我认为有更“干净”的方法来做到这一点。基础单例类和所有其他基础通用页面使得代码更难读,也不容易理解。我们通过应用IoC 容器解决了这个特定问题。不幸的是,由于两个泛型参数,页面对象的继承树仍然相对复杂。

IoC 容器 – 旧版本

public class BasePage<M>
    where M : BasePageElementMap, new()
{
    protected readonly string url;

    public BasePage(string url)
    {
        this.url = url;
    }

    public BasePage()
    {
        this.url = null;
    }

    protected M Map
    {
        get
        {
            return new M();
        }
    }

    public virtual void Navigate(string part = "")
    {
        Driver.Browser.Navigate().GoToUrl(string.Concat(url, part));
    }
}

public class BasePage<M, V> : BasePage<M>
    where M : BasePageElementMap, new()
    where V : BasePageValidator<M>, new()
{
    public BasePage(string url) : base(url)
    {
    }

    public BasePage()
    {
    }

    public V Validate()
    {
        return new V();
    }
}

在就断言方法是否应该属于页面对象进行了一些讨论后,我开始思考如何改进它们。一方面,我希望断言能够被重用;另一方面,它们又似乎不应该直接属于页面对象。我当时认为,因为它们是引入第二个带有额外泛型参数的基类的原因。此外,还有一个新要求——页面需要可互换。总之,如果您有一个页面 A,后来它被弃用,它的替代品是页面 A1,那么在不破坏所有周围代码的情况下,轻松地交换这两个实现应该成为可能。

接口 – 改进版本

public class BingMainPage : BasePage<BingMainPageMap>, IBingMainPage
{
    public BingMainPage(IWebDriver driver)
        : base(driver, new BingMainPageMap(driver))
    {
    }

    public override string Url
    {
        get
        {
            return @"http://www.bing.com/";
        }
    }

    public void Search(string textToType)
    {
        this.Map.SearchBox.Clear();
        this.Map.SearchBox.SendKeys(textToType);
        this.Map.GoButton.Click();
    }
}

为了解决最后一个问题,我们引入了页面对象模式的一个新参与者——页面的接口。它定义了页面应该能够执行哪些操作。

public interface IBingMainPage : IPage
{
    void Search(string textToType);
}

现在,所有依赖于页面的代码都可以将页面用作接口。这意味着,如果您需要替换它,新版本只需要实现相同的接口。

我们做的第二个改进与之前称为 Validators 的类有关。总的来说,它们包含断言方法,所以我们首先决定将它们重命名为以 Asserter 结尾。我们这样做是因为在生产代码中,验证器承担不同的任务,例如验证用户输入,而不是断言事物。

之后,我们应用的另一个大的重构是,Asserter 的方法被用作页面接口的扩展方法。结果,它们可以作为具体页面提供的普通方法使用。

public static class BingMainPageAsserter
{
    public static void AssertResultsCountIsAsExpected(this IBingMainPage page, int expectedCount)
    {
        Assert.AreEqual(page.GetResultsCount(), expectedCount, "The results count is not as expected.");
    }
}

所提出实现的一个缺点是,您始终需要为被断言的元素创建一个包装器方法,因为您无法通过其接口直接访问页面的元素映射。然而,这些想法消除了第二个基页和额外的泛型参数。

public abstract class BasePage<TMap>
    where TMap : BaseElementMap
{
    private readonly TMap map;
    protected IWebDriver driver;

    public BasePage( IWebDriver driver, TMap map)
    {
        this.driver = driver;
        this.map = map;
    }

    internal TMap Map 
    {
        get
        {
            return this.map;
        }
    }

    public abstract string Url { get; }

    public virtual void Open(string part = "")
    {
        this.driver.Navigate().GoToUrl(string.Concat(this.Url, part));
    }
}

从示例中可以看出,与之前的版本相比,继承模型得到了简化。

提供的解决方案的使用很简单。

[TestClass]
public class BingTests 
{
    private IBingMainPage bingMainPage;
    private IWebDriver driver;

    [TestInitialize]
    public void SetupTest()
    {
        driver = new FirefoxDriver();
        bingMainPage = new BingMainPage(driver);
    }

    [TestCleanup]
    public void TeardownTest()
    {
        driver.Quit();
    }

    [TestMethod]
    public void SearchForAutomateThePlanet()
    {
        this.bingMainPage.Open();
        this.bingMainPage.Search("Automate The Planet");
        this.bingMainPage.AssertResultsCountIsAsExpected(264);
    }
}

测试就像之前版本一样使用页面。这里唯一微妙的细节是,如果您想使用扩展断言方法,您需要添加命名空间的 using 语句,它们的类就位于其中。

页面对象 v.2.1 – 公共映射,跳过接口

正如副标题所指出的,页面对象的下一代“版本”将其元素映射暴露给使用该页面的代码。此外,我们决定为每个特定页面创建接口是一种开销。而且,我们发现大多数时候替代页面的接口应该与旧页面不同,因为对页面应用了不同的更改。因此,我们做的第一个细微调整是在基页类中,将 Map 属性标记为 public

public abstract class BasePage<TMap>
    where TMap : BaseElementMap
{
    private readonly TMap map;
    protected IWebDriver driver;

    public BasePage(IWebDriver driver, TMap map)
    {
        this.driver = driver;
        this.map = map;
    }

    public TMap Map 
    {
        get
        {
            return this.map;
        }
    }

    public abstract string Url { get; }

    public virtual void Open(string part = "")
    {
        this.driver.Navigate().GoToUrl(string.Concat(this.Url, part));
    }
}

第二个改变与 Asserter 类有关。现在它们不再扩展页面的接口,而是扩展页面本身。

public static class BingMainPageAsserter
{
    public static void AssertResultsCountIsAsExpected(this BingMainPage page, int expectedCount)
    {
        Assert.AreEqual(page.Map.ResultsCountDiv.Text, expectedCount, 
                        "The results count is not as expected.");
    }
}

测试使用上的唯一区别是,现在您可以直接在测试中访问 Map 的元素。

[TestMethod]
public void SearchForAutomateThePlanet()
{
    this.bingMainPage.Open();
    this.bingMainPage.Map.FeelingLuckyButton.Click();
    this.driver.Navigate().Back();
    this.bingMainPage.Search("Automate The Planet");            
    this.bingMainPage.AssertResultsCountIsAsExpected(264);           
}

在我看来,这只是一个好主意,前提是框架的底层逻辑隐藏在页面对象后面。我的意思是像在真正输入之前清除文本输入框,执行 JavaScript 调用等等操作。

页面对象 v.2.3 – 部分页面

我最喜欢我们设计的页面对象的一点是,元素、页面逻辑和断言被放置在不同的文件中。我相信这使得页面对象更容易理解和阅读。此外,它降低了相关的维护成本。

为了进一步简化继承树,我们决定使用 partial 类而不是泛型基类。通过将 Map 属性设为 public 的小调整,我们认为将映射元素直接放入页面是一个绝妙的主意。然而,如果它们放在同一个文件中,设计就会有与 WebDriver 相同的缺点。这会导致文件变得更大,元素会与页面的服务方法混在一起。我们不希望那样。解决方案是将元素保留在一个单独的文件中,但现在它是主页面类的一个 partial 类。

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

这是新页面映射的外观。它使用了在主页面类中定义的 driver 实例。

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

主页面类也有一些小的改动。现在它继承了 BasePage 的一个更简单的版本,该版本不需要泛型元素映射参数。

新页面对象的使用与之前介绍的完全相同,唯一的区别是现在可以直接从页面实例访问页面的元素。

[TestMethod]
public void SearchForAutomateThePlanet_Second()
{
    this.bingMainPage.Open();
    this.bingMainPage.SearchBox.Clear();
    this.bingMainPage.SearchBox.SendKeys("Automate The Planet");
    this.bingMainPage.GoButton.Click();
    this.bingMainPage.AssertResultsCountIsAsExpected(264);
}

优点

所提出想法的最大优点之一是页面类的单一职责。

在面向对象编程中,单一职责原则规定,每个类都应该只负责功能的一个部分,并且该职责应该完全封装在该类中。

在提出的实现中,map 类只负责定位元素,asserter 类负责断言事物,而页面本身则负责提供服务方法。

到目前为止,“自动化测试中的设计模式”系列

  1. 页面对象模式
  2. 高级页面对象模式
  3. 门面设计模式
  4. Singleton 设计模式
  5. 流畅页面对象模式
  6. IoC 容器与页面对象
  7. 策略设计模式
  8. 高级策略设计模式
  9. 观察者设计模式
  10. 通过事件和委托实现观察者设计模式
  11. 通过 IObservable 和 IObserver 实现观察者设计模式
  12. 装饰器设计模式 - 混合策略
  13. 使代码更具可维护性的页面对象
  14. 改进的自动化测试外观设计模式 v.2.0
  15. 规则设计模式
  16. 规格设计模式
  17. 高级规格设计模式

如果您喜欢我的出版物,请随时订阅
也请点击这些分享按钮。谢谢!

源代码

参考文献

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

文章 让代码更具可维护性的页面对象 最先发布于 Automate The Planet

© . All rights reserved.