微信号:infoqchina

介绍:有内容的技术社区媒体

使用Selenium测试时必需知道的7件事

2015-08-07 12:21 邵思华 译

Selenium是一套用于进行浏览器自动化测试的开源工具集,可进行Web应用的端到端测试。Selenium主要包括两个工具:一是Selenium IDE,这是一个在Firefox上运行的插件,可对用户的行为进行录制与回放,还可以将录制的内容生成代码后在Selenium Remote Control上运行。二是本文的重点Selenium WebDriver(简称WebDriver),这是一个开源的项目,能够让用户编写在各种主流浏览器上运行的互操作代码。目前已经推出了支持C#、Java等语言的类库。W3C的WebDriver规范也正是在这个开源项目的基础上发展起来的。


WebDriver可谓QA工程师进行UI测试最强大的利器,它提供了丰富的API以实现访问DOM、运行JavaScript、模拟键盘输入等操作。利用WebDriver进行编程可实现UI测试的完全自动化,为回归测试、乃至持续集成流程提供了极大的便利性。尽管如此,但使用WebDriver编写测试需要投入大量的时间,并且由于浏览器行为的多样性,以及UI的易变性,需要进行大量的代码维护工作。与应用程序的代码一样,编写测试代码同样需要遵循良好的代码规范与设计,糟糕的代码结构会很快使得测试代码的维护变成一个无底洞,最终被团队无奈地抛弃。


在今年的OpenWest 2015大会上,来自Lucidchart的Jared Yarn进行了一场关于Selenium WebDriver测试方面的演讲,并随后撰文总结了演讲的内容。他首先谈起了所在的团队在使用WebDriver时所遇到的困境,当时他们维护着由大约40个不同开发者编写的300多个测试用例(该团队没有专职的测试人员,测试代码全部由开发者编写),每天的运行都会产生70个左右的错误,这一情况在分配了专门的维护人员之后也没有多少改善。为了彻底改进测试集的可靠性、可伸缩性以及可维护性,Yarn与整个团队一起对整个测试代码结构进行了重构。经过重构后,误判的失败率降到了1%以下,并且编写测试的时间也大大缩短了。


Yarn将这次重构的成功归结为以下七点。


创建Application User对象

团队首先要解决的问题是编写测试所需投入的精力过大,为了克服这一点,他们设计了一些实体对象。首先创建的是一种Application User对象,它代表了网站的后端功能,并且通过一些辅助方法提供了准备测试场景、或是在测试完成前进行teardown(清理)工作的功能。以下是使用这种对象的一个示例:


class EditorPerformanceTest
extends LucidSpec { val user = new ChartUser override def beforeAll() { user.login() user.createDocument() } … override def afterAll() { user.finished() }


通过这种对象的应用,所有的准备工作被简化成两个方法调用(login与createDocument),而teardown中的逻辑则由finished方法实现,因此开发者可以专注于具体的测试逻辑,将精力集中在bug修复或特性的检测。


创建Application Driver对象

WebDriver的API非常丰富,单是定位某个UI元素就有不下20种做法,这种巨大的灵活性也令人望而生畏。有数之不尽的方式可以完成拖放、单击、滚动以及输入等操作。为了简化这一点,Yarn的团队设计了一种Application Driver类,以简化一些最常见的操作。它首先继承自WebDriver类,并引用了Selenium中的Actions类,随后加入了一些方法用于实现最常见的用户操作,例如单击元素与执行脚本等等。可以通过下面这个UML图概括这个类的设计。

其使用方法如下:


def dragAndDrop
(cssFrom: String, cssTo: String) { val elem1 = getElementByCss(cssFrom) val elem2 = getElementByCss(cssTo) actions.dragAndDrop(elem1, elem2)}
def contextClickByCss(css: String) actions.contextClick
(getElementByCss(css))}


通过ID访问DOM对象

在WebDriver测试过程中,如何定位一个DOM元素是最有挑战性的任务之一。常见的方式包括XPath、CSS路径以及各种复杂的CSS选择器(类似于jQuery),但这些方式在元素移动了位置或改变了CSS类名之后就会失效,不得不重新修改代码。因此,Yarn建议使用DOM元素的ID进行定位,这种方式的好处是不受元素所在位置、以及所应用的样式的影响。Yarn的团队随后对产品的某一重要特性进行了UI改版,而由于页面中的ID保持不变,因此测试代码的改动非常之少。


页面对象模式

页面对象模式(Page Object Pattern)是测试代码可维护性的关键因素,这一模式本身非常简单,它表示每个页面应了解如何执行该页面当中的所有操作。举例来说,登录页面知道应当如何提交用户的认证信息、如何点击“忘记密码链接”等等操作。如果将这些功能转移到一个公用的地方,就可以在所有测试中重用这部分功能。以下代码表示了一个文档页面的功能:


 object DocsList extends RetryHelper 
with MainMenu with Page { val actionsPanel = new ActionsPanel val fileBrowser = new FileBrowser val fileTree = new FileTree val sharingPanel = new SharingPanel val invitationPanel = new InvitationPanel


这个页面中的操作非常多,因此Yarn将其分解为多个较小的类,每个类都代表了页面中某个块的功能。它们各自包含在这一区域内可执行的操作的相关方法,正如以下代码所示:


def clickCreateDocument
(implicit user: LucidUser) { doWithRetry() { user.clickElemen
t("new-document-button") }}def selectDocument
(fileNum: Int=0)
(
implicit user: LucidUser) { doWithRetry() { user.driver.getElements
(docIconCss)(fileNum).click() }}def numberOfDocsEquals
(numberOfDocs: Int)
(
implicit user: LucidUser) : Boolean ={ predicateWithRetry
(WebUser.longWaitTime *5, WebUser.waitTime) { numberOfDocuments == numberOfDocs }}



行为的重试

在WebDriver测试过程中,最糟糕的问题在于误判的错误,这为自动化构建过程带来了很大的困难。对于Yarn的团队来说,这个问题也是他们所面对的头号大敌。为了克服这一点,他们为测试加入了重试的功能,使得测试结果得到很大的改善。 以下是这个重试方法的代码:


/*** Try and take an action until it
returns a value or we timeout* @param
maxWaitMillis the maximum amount of time
to keep trying for in milliseconds*
@param pollIntervalMillis the amount of time
to wait between retries in milliseconds*
@param callback a function that gets a
value* @tparam A the type of the callback*
@return whatever the callback returns,
or throws an exception*/
@annotation.
tailrecprivate def retry[A](maxWaitMillis:
Long, pollIntervalMillis: Long)
(
callback: => A): A = { val start = System.currentTimeMillis Try { callback } match { case Success(value)
=> value case Failure(thrown)
=> { val timeForTest = System.
currentTimeMillis - start
val maxTimeToSleep
= Math.
min(maxWaitMillis - pollIntervalMillis,
pollIntervalMillis
)
val timeLeftToSleep
= maxTimeToSleep
- timeForTest if (maxTimeToSleep
<= 0) { throw thrown
} else {
if (timeLeftToSleep > 0)
{ Thread.sleep(timeLeftToSleep) }
retry
(maxWaitMillis - pollIntervalMillis,
pollIntervalMillis
)(callback) } } }}



这段代码的功能是通过一个简单的递归算法执行所传入的实际行为,直到该行为成功,或是运行超时为止。以下是使用这个方法的简单示例:


def numberOfChildren
(implicit user: LucidUser): Int = { getWithRetry() { user.driver.getCssElement
(visibleCss).children.size }}



测试集重试

Yarn的团队所做的最后一项改善是配置测试集的重试,测试集重试会将失败的测试缓存起来,然后重新运行这些失败的测试。只要在后续的重试中有一次成功,这项测试就会被认为通过。否则将继续重试,直到重试次数达到上限为止。 Yarn的做法是尽量将一些依赖于第三方功能的行为区分开来,特意为这些功能的集成编写非常健壮的代码似乎没有什么意义,因此可以将它们放到一个可重试的测试集中。对于他们来说,重试的目的不是为了修复测试代码中的问题,而是为了消除测试报告中由误判所带来的影响。


创造乐趣

Selenium的开发很容易令人感到疲惫,许多测试会无故地失败,让这些测试得到正确的结果是非常繁琐的工作,重复性的样板代码令人提不起兴致。而在Yarn的团队建立了一个可靠的、可维护以及可伸缩的框架之后,工作就变得有趣起来了。各种有趣的想法层出不穷,有一位开发者实现了对绘画canvas截图并上传至Amazon S3服务的功能,随后又加入了一个截图比较的工具以实现图片比较测试。其它令人印象深刻的测试还包括与Google Drive、Yahoo与Google的单点登录等功能的整合。整个测试工作开始变得生动起来,这也为团队最终实现了重构的目标带来了极大的推动力。


相关文章推荐

简化你的Java代码,让工作更高效|语言

语言|Java内存模型修订了!

怎么用API网关构建微服务|架构


投稿请联系:

邮箱:lillian@infoq.com QQ:1073600161

版权归属InfoQ,禁止私自抄袭转载。

回复关键词React | 架构师 | 运维 | 云 | 开源 | 物联网 | Kubernetes | 架构 | 人工智能 | Kafka | Docker | Netty | CoreOS | QCon | Github | Swift | 敏捷 | 语言 | 程序员


有话想说?!戳“写评论”👇

 
InfoQ 更多文章 Facebook如何实现PB级别数据库自动化备份 学术派Google软件工程师Matt Welsh谈移动开发趋势 Spotify为什么要使用一些“无聊”的技术? 妹纸们放假了,汉纸们做啥? 大多数重构可以避免
猜您喜欢 《开发者头条》Android & iOS 版 v2.3.0 发布啦!赶快更新吧! 36大数据“大数据应用与实践”沙龙本周日正式举行 EJ系列16-18条 为什么我看不懂你的代码 Swift 2.0 到底「新」在哪?