Selenium 的发展经历了三个阶段:Selenium Core、Selenium RC 和 Selenium WebDriver。本文将依次介绍每个阶段的工作原理,如有错误,请及时指正。
提示:Selenium Core 用户不直接接触,而 Selenium RC 已经过时,不感兴趣的同学可以直接看第三节 Selenium WebDriver。
术语列表:
术语 | 全称 | 中文全称/简介 |
---|---|---|
AUT | Application Under Test | 被测应用 |
Selenium | Selenium | 一款跨浏览器自动化工具 |
Selenium Core | Selenium Core | Selenium 第一代版本,简称 Selenium 1.0,于 2004 年发布 |
Selenium RC | Selenium Remote Control | 基于 Selenium Core 的改进版,属于 Selenium 1.0 |
WebDriver | WebDriver | 和 Selenium Core 类似的跨浏览器自动化工具,于 2007 年首次发布源码 |
w3c WebDriver | w3c WebDriver | 由 WebDriver 发展而来的浏览器自动化协议,有时简称为 WebDriver 协议/规范/API |
Selenium WebDriver | Selenium WebDriver | 2009年8月由 Selenium 1.0 和 WebDriver 项目合并而成,遵循 w3c WebDriver 协议,早期又称作 Selenium 2.0 |
Driver | Driver/Browser Driver | 浏览器驱动,实现了 w3c WebDriver 接口,每个浏览器都有自己的驱动程序 |
DriverService | DriverService | 驱动服务,运行驱动程序后所起的 HTTP 服务,接收 WebDriver 接口调用后操作浏览器 |
Selenium Core
在 2004 年 Internet Explorer 有 93.25% 的市场占有率,当时的开源测试工具要么关注单个浏览器的测试(如 IE),要么是浏览器模拟工具(如 HttpUnit )。没有开源自动化测试工具能支持多浏览器的测试,手工测试执行需要耗费大量的时间和精力。
提示:HttpUnit 是浏览器模拟工具,测试脚本与 HttpUnit 交互,HttpUnit 和真实服务端交互,不经过真实的浏览器,即不是模拟用户操作真实的浏览器。
初步想法
不过所有浏览器都支持 JavaScript,这为开发支持多浏览器的自动化工具提供了可能。受 Fit: Framework for Integrated Test 启发,ThoughtWorks 的 Jason Huggins 和他的团队想到了一个方案,使用基于表格的关键字驱动语法来编写测试用例。相比原生的 JavaScript 脚本,用户只需要有限的编程能力,另外用例也更容易理解和方便维护。
基于表格的关键字语法(一个典型的登录操作):
Command | Target | Value |
---|---|---|
open | http://example.com/page1.html | |
type | username | testUser |
type | password | testPasword |
clickAndWait | submitButton | |
verifyTextPresent | Welcome, testUser! |
- 第一列:命令的名称,如 open、type、click、submit 等等。
- 第二列:目标,如元素定位符、URL 等。
- 第三列:可选的值(如 click 命令不需要值,type 命令可能需要输入一些值)。
如何实现
开发团队里有几位成员对 Closure Library 非常熟悉,而且 Closure Library 编译的唯一输出语言是 JavaScript 。所以使用 Closure Library 来开发 Selenium Core 是个合适的选择。
与很多大型项目一样,Selenium Core 采用了分层构建的架构。
最底层是 Google 开源的 Closure Library,它是一个模块化的、跨浏览器的底层 JavaScript 库,模块化使每个源文件可以聚焦某个功能并且尽可能小,跨浏览器可以帮其屏蔽掉很多浏览器兼容性问题。
中间层是一个包含大量函数的工具层,提供了从简单到复杂的操作,如:获取元素属性值、判断一个元素对用户是否可见、使用合成事件模拟用户点击。该层可以被看作是浏览器自动化的最小操作单元,因此也被称为浏览器自动化原子 Atoms
或 atoms
。
最上层是由 Atoms
组成的适配层,用于实现 Selenium Core
约定的 API,即前面表格里的命令。
至此,只要将表格内容解析为 Selenium Core
的 API,一个完整的跨浏览器自动化工具就实现了。
用于实战
Selenium Core 使用纯 JavaScript 编写,而 JavaScript 存在同源策略的问题,因此使用时需要将 Core 和测试用例部署在与被测应用相同的服务器上(只要被测应用和测试脚本同源就可以)。这也意味着,你无法测试别人的网站,比如 https://www.baidu.com。
同源策略:
- Same-origin policy
- 浏览器同源策略及跨域的解决方法
- nginx解决跨域问题
- 补充说明:只有 JavaScript 脚本受同源策略限制,HTML 标签( script、link、img等)不受此影响
Selenium Core 的使用步骤:
- 下载 Selenium Core 的压缩文件并解压,如:
selenium-core-1.0.1.zip
。 - 复制
core
文件夹到应用服务器的目录。 - 访问
http://<webservername>:<port>/[path/]core/TestRunner.html
页面运行测试。被测应用有两种运行方式,单窗口模式:在 TestRunner 页面下方使用iframe
嵌入被测应用,多窗口模式:在新窗口中打开被测应用。访问 https://www.qadoc.org/edu/cat... 查看示例。
扩展:使用 HTA 模式(将TestRunner.html
重命名为TestRunner.hta
)可以测试其他网站,但 HTA 仅支持 Windows 上的 IE 浏览器。
最后总结一下:
- 在与
core
文件夹同级的tests
文件夹下,使用 HTML 编写基于表格的测试用例页面,并加入测试套件页面。 - 部署
core
和tests
文件夹到应用服务器的目录,避开同源策略限制,也因此无法测试其他非同源网站,包括页面跳转后的非同源网站。 - 访问
http://<webservername>:<port>/[path/]core/TestRunner.html
页面准备运行测试。 - 选择一个测试套件或测试用例,点击
Run All tests
或Run the Selected test
运行测试用例。 Selenium Core
获取测试套件的所有测试用例或当前测试用例,解析表格的每一行(固定三列),按行依次调用Selenium Core
API 执行。- 每个 API 调用
Atoms
层函数,Atoms
层调用Closure Library
函数,因为Closure Library
和JavaScript
都是跨浏览器的,所以Selenium Core
支持多个浏览器。
扩展:虽然Selenium Core
本身受限于同源策略,无法测试其他非同源网站,但幸运的是,浏览器插件的 JavaScript 代码并没有这个限制,所以基于Selenium Core
的 Selenium IDE 支持任何网站的测试。
Selenium RC
上节我们了解到,Selenium Core
虽然满足了跨浏览器自动化的基本需求,但仍然有一些不足:
- 使用 HTML 编写测试用例,由于没有专门针对 HTML 的用例编写工具,当用例规模较大时,编写和维护都是一件头疼的事。
- 测试工具和测试用例需要部署在应用服务器上,这对被测应用有一定的侵入性,另外不是所有情况下都能在应用服务器上部署测试代码。
- 无法测试与被测应用非同源的网站,这个缺点在今天,显得更为明显,现今不少网站都流行使用子域名,这导致页面跳转后将无法继续执行测试。
淘宝部分子域名示例:
为了解决这些问题,Selenium RC 随之诞生。
如何避开同源策略
为了解决上面的问题,开发团队为 Selenium 写了一个 HTTP 代理服务器,这样 Selenium 可以拦截一切 HTTP 请求。
HTTP 代理:HTTP 代理原理及实现(一)
关于 Selenium RC 架构,官网有详细的描述,这里偷下懒,做下翻译和补充。
代理注入
下载解压后,启动 Selenium RC Server:
java -jar selenium-server.jar -interactive -browserSideLog -proxyInjectionMode -debug -log seleniumrc.log
当我们执行测试用例(使用编程语言编写的测试脚本)时,下面的步骤将发生:
- 客户端驱动(即 Selenium 的某个编程语言的客户端库)与 Selenium RC 服务器建立连接。
- Selenium RC 服务器使用特定的 URL 启动浏览器(或重复使用之前的浏览器实例),该页面引入了 Selenium-Core 的所有文件(JS、CSS 文件)。
- 客户端驱动通过 HTTP 发送一条 Selenium 命令给 RC 服务器。
- 服务器解释命令,并触发相应的 JavaScript 在浏览器中执行命令。第一条命令,通常是 open AUT 的一个页面。
- 浏览器收到 open 请求后,向 RC 服务器(作为浏览器的代理)请求页面内容。
- RC 服务器向 Web 服务器(AUT 的服务器)请求页面,请求完成后,RC 服务器将 Selenium Core 注入该页面并发送给浏览器(这让 Selenium Core 看起来和该页面同源)。
- 浏览器接收该页面并在 frame 或 window 中渲染页面。
补充几点官网没有解释的内容:
Selenium RC 如何启动浏览器
使用命令行方式启动浏览器,启动命令的格式如下:
# 格式:浏览器程序文件路径 + 命令参数
# 示例
"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe"
--disable-hang-monitor
--disable-metrics
--disable-popup-blocking
--disable-prompt-on-repost
--proxy-server="localhost:4444"
--start-maximized
--user-data-dir="C:\Users\Think\AppData\Local\Temp\customProfileDir7a0b6e794a9445338159f582e1b8e3aa"
http://oktools.xyz/selenium-server/core/RemoteRunner.html?sessionId=7a0b6e794a9445338159f582e1b8e3aa&multiWindow=false&baseUrl=http%3A%2F%2Foktools.xyz%2F&debugMode=true
更多细节可以翻阅 org.openqa.selenium.server.browserlaunchers.AbstractBrowserLauncher#launch
实现类对应的方法。
Selenium RC 如何和浏览器通信
这个问题乍看起来好像不是问题,但仔细想,浏览器并不是服务端,即没有处理请求的能力,所以 RC 无法通知浏览器。有人可能会想到使用 websocket,但 websocket 2011 年才成为浏览器标准,而在 2004 年的时候,Selenium RC 显然还无法使用它。
当时有一种叫 Comet 的技术,Selenium RC 最终选用的是 XMLHttpRequest 长轮询,用于保持和浏览器的连接。下图是一个简单的示例,客户端(图示中使用的是 Selenium Sever 的交互模式,功能和客户端相同)先后发起了两个命令:getNewBrowserSession
、open
。
服务器收到 getNewBrowserSession
命令后,启动浏览器,浏览器访问 RemoteRunner.html,该页面通过 script 等标签引入了 Selenium Core。
- 当浏览器加载 RemoteRunner.html 页面时触发 onload 事件, Selenium Core 发起一个 seleniumStart 的 HTTP 请求,服务器返回一个
getTitle
命令。 - Selenium Core 收到响应后,执行响应中的命令,继续发出请求(携带刚才的命令执行结果),服务器返回
setContext
命令,通知 Selenium Core 设置上下文。 - Selenium Core 收到响应后,执行响应中的命令,发出重试请求。服务器如果 10001ms 内没有收到客户端的命令,便结束此次请求,返回 retryLast 命令,即继续重试。
- Selenium Core 收到响应后,重复第三步,直到客户端给服务器发送新命令。
整个 XMLHttpRequest 长轮询的过程相当于是一个 Response/Request 模式,服务器将要通知浏览器的内容放入 Response Body 中,浏览器将本次 Response 的处理结果放入下次的 Request URL 和 Body中,因此对于服务器来说,每次 HTTP 响应相当于服务器的请求,每次 HTTP 请求相当于服务器的响应。
由于文章篇幅有限,open 命令的执行过程就不介绍了。上图的 uniqueId
是什么用途呢?每当页面跳转或刷新后,意味着上个页面会关闭,新页面需要继续保持和服务器的连接,这里的 uniqueId
可以理解成页面的标识,每当上个页面关闭后,下个页面就会使用新的 uniqueId
发起请求,同时 sequenceNumber
被重置为 0。
提升浏览器权限
该模式和代理注入非常类似,主要区别在于浏览器以一个被称为 Heightened Privileges
的特殊模式启动,它允许网站做一些通常不被允许的事(比如 XSS、填充文件上传输入框以及对 Selenium 很有用的东西)。该模式下,Selenium Core 可以直接打开 AUT 并读取它的内容或与它的内容交互,而不必通过 Selenium RC 服务器访问整个 AUT。
Selenium WebDriver
早期的 WebDriver
早期的 WebDriver
是和 Selenium Core
几乎同时期的来自 ThoughtWorks 的浏览器自动化工具,也是基于 Atoms
构建,这和后来我们所熟悉的 w3c WebDriver
协议有些不同。
Selenium Core
和 WebDriver
的设计理念有很大不同。WebDriver
的开发人员倾向于向用户隐藏其并不关心的很多细节,提供尽可能简单的 API,好让用户聚焦在用例设计和发现 Bug 上,正如他们擅长的那样,而 Selenium Core
则是给用户提供低级别的 API 。以下是 Selenium Core
用于设置 input 元素值的方法:
- type
- typeKeys
- keydown
- keypress
- keyup
- keydownNative
- keypressNative
- keyupNative
- attachFile
它们等价于 WebDriver
的 sendKeys
API,正如前面说的一样,WebDriver
努力模仿访问被测应用的用户。xxx 和 xxxNative 的区别在于,前者使用合成事件,后者通过 java.awt.Robot
模拟键盘输入来模拟用户操作。
w3c WebDriver 协议
讲述 Selenium WebDriver
原理之前,我们先来看下 w3c WebDriver
协议,这里以 ChromeDriver 为例。
术语解释:
- W3C WebDriver 是一个浏览器协议,又称
WebDriver 协议
/WebDriver 规范
/WebDriver API
- Driver 是 WebDriver API 的特定实现,比如 Chrome 浏览器的 ChromeDriver。
- ChromeDriver 是一个可以独立运行的服务器程序,它实现了 WebDriver 协议。
- Selenium WebDriver 是一个基于 WebDriver 协议的 Web 自动化框架。
启动 ChromeDriver
在命令行窗口执行以下命令启动 ChromeDriver。
# 为了方便,建议先切换到 exe 文件所在目录
chromedriver_80.exe -port=12345
ChromeDriver 启动成功后,将得到一个服务器访问地址 http://localhost:12345
。打开任务管理器,我们可以看到 chromedriver_80.exe
进程。
访问 ChromeDriver
1、调用 New Session 接口,创建一个 Session,返回 sessionId=be92a2485bcdb5ca0d3bc44bd0b00a8d。与此同时将打开一个 Chrome 浏览器窗口。
endpoint\_webdriver=http://localhost:12345
思考题:如果再次调用该接口会发生什么呢?
2、调用 Navigate To 接口,访问百度网站,等价于 Java 客户端的 driver.get("https://www.baidu.com")
。
执行结果:
其他接口类似,不再一一举例。这些步骤是不是和使用 Java/Python 编写测试步骤时似曾相识?总结起来就是两步:
- 第一步:通过 WebDriver 驱动启动一个服务端(每个驱动操作与之对应的浏览器,如 Chrome 驱动操作 Chrome 浏览器)。
- 第二步:客户端调用服务端 HTTP 接口,服务端收到请求后解析请求,执行对应的浏览器操作。
浏览器驱动大部分是各浏览器厂商根据 WebDriver 规范实现的,所以 Selenium 不再需要直接操作浏览器,而是通过 HTTP 接口向驱动发出符合 WebDriver 规范的指令。
有一点需要注意,虽然低版本的 Firefox(47及以下) 和 Safari 不需要单独下载驱动程序,但这不是说 Firefox 和 Safari 不需要 WebDriver 驱动,而是因为在 Selenium 客户端中内置了浏览器的 webdriver 扩展,这些浏览器扩展起到了和驱动相同的作用,同样遵循 W3C WebDriver 协议。
从 Selenium 的源码中可以发现这些特殊情况:
- Selenium2和3中低版本 Firefox 使用了
webdriver.xpi
,通过 HTTP 通信。 - Selenium2 中 Safari 使用了
client.js
,通过WebSocket
通信。 Selenium3 中 Safari 使用苹果系统自带的 safaridriver,通过 HTTP 通信。
- Safari 发布版驱动路径:
/usr/bin/safaridriver
- Safari 技术预览版驱动路径:
/Applications/Safari Technology Preview.app/Contents/MacOS/safaridriver
- Safari 发布版驱动路径:
Selenium WebDriver
了解了 WebDriver 协议后,我们回过头来看 Selenium WebDriver 的工作原理就简单很多了。
如果我们有一个客户端库,负责浏览器驱动的启动、管理,HTTP 请求的组装、响应的处理等诸多和测试脚本本身无关的事,测试用例的编写将变得更加容易。而 Selenium WebDriver 就提供了一系列这样的客户端库,除此之外还提供了 Selenium Grid 用于分布式执行。
从上图中可以看出,Java 编写的测试脚本中的命令由 Java 客户端转为 HTTP 请求并访问 WebDriver 服务器(驱动服务),浏览器驱动收到 HTTP 请求后,操作浏览器,就用网站用户那样。
如果你读过上面 Selenium RC 的部分,你就会发现,Selenium WebDriver 相比 Selenium RC,从架构上来说要简单很多,API 也更加简洁。这对用户和 Selenium 的开发者来说,都是一个质的飞跃。如果你考虑到 Selenium 要支持 X 个操作系统、Y 种浏览器和 Z 种编程语言,你就能理解 Selenium 的开发工作量有多大了,在 Selenium RC 时代,大量代码和编程语言绑定(比如 XHR 的长轮询、SSL 证书管理),这让每次迭代更新都很痛苦。
上面的 Selenium WebDriver 原理图,等价于官网的这张图。
除此之外,还可以有不同的部署方式,使用 Remote WebDriver
,操作远程主机上的浏览器。
通过 Selenium Server 或 Selenium Grid 与驱动通信。
Selenium firefox xpi
因为对 Firefox 的支持,已经从内置的 firefox xpi 变为和其他驱动类似的 geckodriver,所以不再过多介绍。如果你对 Selenium 客户端中内置的 firefox xpi 插件的工作机制感兴趣,可以阅读参考资料 [1] 中的 16.6. The Remote Driver, and the Firefox Driver in Particular
部分。
参考资料:
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。