2天时间,聊今年最热的 Agent、上下文工程、AI 产品创新等话题。2025 年最后一场~ 了解详情
写点什么

使用 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:165543
用户头像

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

关注

评论

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

图灵8月书讯 | 10 本新书上市,本本精选

图灵社区

新书推荐

从实验室到用户桌面,深度解析AI 落地实践之路

云智慧AIOps社区

人工智能 机器学习 AI 算法实践 智能运维AIOps

泰禾智能:智能改变未来,成就工业设备行业佼佼者

科技怪咖

105份墨天轮“国产化迁移”干货文档汇总(含TiDB、openGauss、上云等)

墨天轮

数据库 阿里云 TiDB 国产数据库 达梦

如何应对软件可变性?这4种常用的方法肯定要知道

华为云开发者联盟

云计算 软件 后端

阿里云 ACK 容器服务生产级可观测体系建设实践

阿里巴巴中间件

阿里云 容器 云原生 可观测

RT-Thread记录(十四、I/O 设备模型之ADC设备)

矜辰所致

RT-Thread 8月月更 ADC

技术分享| anyRTC 8月更新迭代

anyRTC开发者

音视频 WebRTC 视频通话 实时消息 抓拍图片

计费准确率高达100%,是如何实现的?

科技怪咖

打破数字化盐碱地:传统型企业如何踏稳数字化转型第一步

科技怪咖

保利威荣获「年度最佳直播服务商」!引领视频直播行业创新发展

科技怪咖

秋招涨薪攻略!拿着GitHub标星80k的阿里性能优化笔记,把项目优化个遍

退休的汤姆

Java 阿里 社招 Java工程师 秋招

马蹄链Dapp系统开发部署技术

薇電13242772558

dapp

文本纠错:提升OCR任务准确率的方法

合合技术团队

人工智能 文字识别 OCR 合合信息

Rewind the PlanetScale Rewind | 拆解硅谷当红科技公司如何做 Product Marketing

Bytebase

这三道MySQL面试题,99%的人答不出来!

程序员小毕

Java MySQL 数据库 程序员 面试

B站基于Iceberg+Alluxio助力湖仓一体项目落地实践

Alluxio

B站 iceberg Alluxio 湖仓一体 8月月更

KubeEdge边缘计算在顺丰科技工业物联网中的实践

华为云开发者联盟

云计算 云原生 工业物联网 顺丰

打造耳聪目明的MES系统,你准备好了吗?

科技怪咖

MobTech ShareSDK 高级接口及配置

MobTech袤博科技

android MobTech袤博科技 sharesdk

教你使用Jupyter可视化查询语句的语法树

华为云开发者联盟

人工智能

从零到一,教你搭建「以文搜图」搜索服务(一)

Zilliz

人工智能 机器学习 深度学习

​Black Hat 2022 聚焦软件供应链安全

SEAL安全

网络安全 软件供应链安全

Kruise Rollout v0.2.0 版本发布:支持 Gateway API、StatefulSet 分批发布等能力

阿里巴巴中间件

阿里云 开源 容器 云原生 OpenKruise

1 分钟在 Serverless 上部署现代化 Deno Web 应用

阿里巴巴中间件

阿里云 Serverless 云原生

何去何从的并行计算

博文视点Broadview

解决 NGINX LDAP 参考实施中的安全问题

NGINX开源社区

nginx HTTP 安全漏洞 ldap 身份验证

“似水无形” 的小程序化技术

Speedoooo

小程序 小程序管理平台 轻应用 快应用 小程序平台

左邻云安全 | 全程守护,让园区安全看得见

科技怪咖

本周四晚19:00知识赋能第七期第3课丨OpenHarmony WiFi扫描仪实现

OpenHarmony开发者

OpenHarmony

MobTech ShareSDK Android端微信分享小程序

MobTech袤博科技

微信小程序 android sdk

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