原文链接:Page Object

当你在为一个 Web 页面编写测试用例的时候,你需要通过该页面的元素来点击链接并确定显示的内容。然而,如果你直接在测试用例中操作 HTML 元素,那么你的测试用例在应对 UI 变化的时候会很脆弱。一个页面对象会把 HTML 页面或者页面片段封装在特定的应用 API 中,使得你在操作页面元素的时候,不需要在 HTML 文档上到处搜索。

页面对象模式的基本原则是它应该使得软件客户端能像真实的人一样去看和做事,它还应该提供一个容易编程的接口,隐藏掉视窗下的小部件。因此,为了访问一个文本字段,你应该有一个入口方法可供调用,这个方法返回一个字符串,复选框应该使用 booleans,而按钮应该被一个面向动作的方法名所表示。这个页面对象应该把寻找和操作 GUI 控件本身的数据的机制封装起来。一个好的原则是,如果具体的控件变化了,页面对象的接口不应该变化。

尽管使用 “页面” 对象这个词,但是通常不应该为每一个页面都创建一个对象,而是应该为页面上的重要元素创建对象。[1] 因此,可以为一个展示多个专辑的页面创建一个包含若干个专辑页面对象的专辑列表页面对象。我们可能还会创建一个页眉页面对象和页脚页面对象。也就是说,复杂 UI 的一些层级结构只是为了构建 UI 而存在,这一类复合的结构不应该被创建的页面对象所揭示。原则上只需要为那些对使用应用的用户来说有意义的页面结构建模。

同样的,如果你导航到另一个页面,原始的页面对象应该返回另一个表示新打开的页面的页面对象。[2] 一般来说,页面对象的操作应该返回基本类型(比如字符串、日期)或者其他的页面对象。

关于页面对象应该自身包含断言,还是仅提供数据给测试脚本作断言,有不同的观点。断言应该包含在页面对象中的倡导者认为,这样做可以避免在测试脚本中重复断言,从而更好地提供错误信息,以及支持一个更TellDontAsk风格的 API。而提倡页面对象不应该包含断言的人认为,页面对象包含断言会把页面对象负责提供访问页面数据的责任跟断言逻辑混在一起,最终导致页面对象变得臃肿。

我支持页面对象不包含断言的做法。我觉得你可以通过为公共断言提供断言库的方式避免重复断言,这样做同样使得诊断程序更容易。[3]

页面对象通常用于测试,但是它们不应该自己做断言,它们的职责是提供访问底层页面状态的入口,而执行断言逻辑是测试客户端的职责。

我上面以 HTML 页面为例描述了这个模式,但是这个模式同样适用于任何 UI 技术。我已经见识过这种模式非常有效地用于隐藏 Java swing UI 的细节,我毫不怀疑它也可以广泛运用于其他现存的 UI 框架。

并发问题是页面对象可以封装的另一个主题。这可能涉及隐藏用户看起来不是异步的异步操作中的异步性,还可能涉及当你不得不担心在 UI 和工作线程之间分配行为的时候,它能把 UI 框架中的线程问题封装起来。

页面对象模式在测试中使用最多,但也可以被用于在应用程序之上提供脚本接口。一般来说,最好在 UI 之下放置脚本接口,这样做通常能减少复杂性,而且更快。然而,对于将很多行为都塞进 UI 的应用程序而言,使用页面对象模式可能是对这项烂工作的修补。(但是,如果可以,你可以考虑将这个逻辑移除,这样对脚本和 UI 的长期健康都更好。)

使用某种形式的领域特定语言来编写测试是很常见的,比如 Cucumber 或者一种内部的 DSL。如果你这样做了,最好是将测试 DSL 放在页面对象之上,这样你就可以有一个解析器用于将 DSL 描述转换成对页面对象的调用。

致力于将逻辑和 UI 元素分离的模式(比如Presentation Model, Supervising Controller, Passive View)使得通过 UI 进行测试变得不那么有用,相应地也减少了对页面对象模式的需求。

页面对象是一个典型的封装案例,它们对其他组件(测试脚本)隐藏了 UI 结构和部件。问一问你自己,我怎么对软件其他部分隐藏一些细节?在开发的过程中寻找这样的场景是一个很好的设计原则。与任何封装一样,这样做会产生两个好处。第一点,我已经强调过,通过将操作 UI 的逻辑限制在一个地方,你在修改它们的时候,就不会影响系统中的其他部分。另一个随之而来的好处是会使得客户端测试脚本更容易理解,因为它此时只包含测试意图,而不会被 UI 细节搞得混乱。

延伸阅读

我第一次描述这种模式,是使用了Window Driver这个名称。然而打那以后,“页面对象” 这个词因为 Selenium Web 测试框架火了起来,也就变成了惯用的名称。

Selenium's wiki强烈鼓励使用页面对象模式,而且提供了具体怎么使用的建议。Selenium 也支持页面对象不包含断言。

有一个团队测量了在软件升级之后,更新 Selenium 测试套件的两个版本的时间。他们发现使用页面对象的版本在第一个测试用例上花费的时间稍长,但是在后续的测试用例中,花费的时间会少很多。更多信息见Leotta et al, “Improving test suites maintainability with the page object pattern”, ICSTW 2013

注释

1:有一种争论说 “页面对象” 这个名称有误导性,因为它会让人们觉得应该仅为每一个页面创建一个页面对象,使用 “面板对象” 这个名称可能更好,但是 “页面对象” 这个词已经被接受了。TwoHardThings这篇文章阐述了为什么命名很难。

2:通常建议页面对象应该负责为导航这一类操作创建另一个页面对象。然而一些从业者更倾向于页面对象返回一个通用浏览器上下文,然后由测试脚本再基于测试的流程(特别是条件性的流程)控制在这个上下文之上应该创建哪一个页面对象。他们的偏好基于这样的事实:测试脚本知道他们期望的下一个页面是哪一个,因此不需要页面对象本身再重复这个信息。特别是他们在使用静态类型语言的时候,他们的这种倾向性更强了,因为这些语言通常在类型签名中就揭示了页面导航。

3:即使像我这样支持页面对象不包含断言的风格的人,也认为有一种断言可以被包含在页面对象中:那些检查页面或者应用程序中的不变量的断言。而测试脚本所探查的特定事物的断言则应该排除在外。


↙↙↙阅读原文可查看相关链接,并与作者交流