写点什么

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

  • 2015-07-31
  • 本文字数:3658 字

    阅读完需:约 12 分钟

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.clickElement("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.tailrec
private 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 的单点登录等功能的整合。整个测试工作开始变得生动起来,这也为团队最终实现了重构的目标带来了极大的推动力。


感谢徐川对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ @丁晓昀),微信(微信号: InfoQChina )关注我们,并与我们的编辑和其他读者朋友交流(欢迎加入 InfoQ 读者交流群)。

2015-07-31 08:165165
用户头像

发布了 428 篇内容, 共 184.0 次阅读, 收获喜欢 39 次。

关注

评论

发布
暂无评论
发现更多内容

洞见科技纪凯:基于隐私计算的「客户增长」生态

洞见科技

金融科技 隐私计算

作为Java程序员连Redis都不会?阿里架构师带你深入“解剖”Redis

Java浪潮

redis JAVA开发 java程序员

[数据分析]-音频分析-BirdCLE-1

浩波的笔记

人工智能 AI 数据分析

进阶中的程序员:深入解析 Spring boot(含PDF文档分享)

Java浪潮

spring 编程 JAVA开发 springboot

黄吉:如何适配OpenHarmony自有音频框架ADM?

OpenHarmony开发者

OpenHarmony

报告解读下载 | 5月《中国数据库行业分析报告》重磅发布!精彩抢先看!

墨天轮

数据库 oracle opengauss TiDB 国产数据库

如何制作优秀的产品说明手册?

小炮

产品说明手册

GAIA数据集V1.10更新

云智慧AIOps社区

运维 AIOPS 数据集

微服务网关除了zuul、spring cloud gateway还有更出色的

爱好编程进阶

Java 程序员 后端开发

CRM系统的18个关键功能

低代码小观

CRM 客户关系管理 企业管理系统 CRM系统 客户关系管理系统

SpringCloud 中 Gateway 是如何关联对应的 GatewayFilterFactory 和 参数解析的

gin

Gateway SpringCould

阿里Java面试必问:JVM与性能优化+Redis+设计模式+分布式

Java浪潮

编程 架构

参与 Apache 顶级开源项目的 N 种方式,Apache Dubbo Samples SIG 成立!

爱好编程进阶

Java 程序员 后端开发

AI简报:Blind超分KernelGAN

AIWeker

人工智能 深度学习 机器视觉 5月月更 超分

原来,嵌入式BI方案的核心差异全在这,教你如何评估!

葡萄城技术团队

数据分析 BI 嵌入式软件 核心差异

云原生基础治理平台SchedulX V1.2.0正式发布,助力企业降本增效

星汉未来

运维 云原生 星汉未来

这么好用的低延时直播,网易云信竟然将它开源了?!

网易云信

开源 WebRTC 音视频技术

手把手推导Back Propagation

OneFlow

人工智能 神经网络 深度学习 Back Propagation 推导过程

深入浅出Redis丨阿里架构师的Redis实战心得,让你毫不费力的学习

Java浪潮

redis java程序员 java架构

再见了收费的Navicat!操作所有数据库有DBeaver就够了

爱好编程进阶

Java 程序员 后端开发

字节面试到底有多难,一个Hadoop源码就拦住了百分之90的人群

爱好编程进阶

Java 程序员 后端开发

堡垒机是服务器吗?两者有区别吗?

行云管家

运维 网络安全 服务器 堡垒机

阿联酋航空与华为进一步深化合作伙伴关系

最新动态

网络协议之:memcached binary protocol详解

程序那些事

Java 网络协议 程序那些事 5月月更

关于MySQL的一些骚操作——提升正确性

爱好编程进阶

Java 程序员 后端开发

运维审计堡垒机哪款好?报价贵吗?

行云管家

网络安全 堡垒机 IT运维 运维审计

毕业总结

凌波微步

架构训练营

Hacker 资讯 | 5 月上旬区块链黑客松活动汇总

One Block Community

区块链

分布式事务及其一致性协议

爱好编程进阶

Java 程序员 后端开发

导师男团来袭 | 开源之夏2022,与Alluxio一起探索数据编排的奇妙世界

Alluxio

开源 大学生 #开源项目 开源之夏 数据编排

借鉴这份Java进阶架构师之路的核心知识,我成功收获蚂蚁金服、阿里、美团等Offer

Java浪潮

Java 架构 java架构

使用Selenium测试时必需知道的7件事_语言 & 开发_邵思华_InfoQ精选文章