测试 Web 应用程序可能是一项挑战。与大多数其他类型的软件不同,它们可以在多种平台和设备上运行。无论外形尺寸或浏览器选择如何,它们都必须保持健壮。
我们知道这对开发人员来说是一个问题:当 MDN 开发者需求评估 询问 Web 开发者他们最头疼的问题时,跨浏览器测试在 2019 年 和 2020 年 的前五名中都名列其中。
对 2020 年结果的分析揭示了一个 子组,其中包含 13% 的受访者,他们认为编写和运行测试的困难是他们在 Web 平台上遇到的最大难题。
在 Mozilla,我们将其视为行动号召。凭借我们致力于构建更美好互联网的承诺,我们希望为 Web 开发者提供他们构建出色的 Web 体验所需的工具,包括出色的测试工具。
在本系列文章中,我们将探讨当前的 Web 应用程序测试环境,并说明 Firefox 今天正在做什么来让开发人员可以在 Firefox 中运行更多类型的测试。
WebDriver 标准
大多数当前的跨浏览器测试自动化使用 WebDriver,这是一个用于浏览器自动化的 W3C 规范。WebDriver 使用的协议起源于 Selenium,它是最古老、最流行的浏览器自动化工具之一。
要了解 WebDriver 的功能和局限性,让我们深入了解一下它在幕后是如何工作的。
WebDriver 提供了一个基于 HTTP 的同步命令/响应协议。在这个模型中,像 Selenium 这样的客户端(在 WebDriver 行话中称为本地端)使用一组固定的步骤与远程端 HTTP 服务器通信。
- 本地端发送一个表示 WebDriver 命令的 HTTP 请求到远程端。
- 远程端采取实现特定的步骤来执行该命令,遵循 WebDriver 规范的要求。
- 远程端将 HTTP 响应返回给本地端。
这个远程端 HTTP 服务器可以内置到浏览器本身中,但最常见的设置是所有 HTTP 处理都在浏览器特定的驱动程序二进制文件中进行。它接受 WebDriver HTTP 请求并将它们转换为浏览器可以使用的内部格式。
例如,在自动执行 Firefox 时,geckodriver 将 WebDriver 消息转换为 Gecko 的自定义 Marionette 协议,反之亦然。ChromeDriver 和 SafariDriver 以类似的方式工作,每个都使用与其关联的浏览器特定的内部协议。
示例:一个简单的测试脚本
为了更好地理解这一点,让我们举一个简单的例子:导航到一个页面、查找一个元素并测试该元素上的一个属性。从测试作者的角度来看,实现此操作的代码可能如下所示
browser.go("http://localhost:8000")
element = browser.querySelectorAll(".test")[0]
assert element.tag == "div"
本示例中的每一行代码都会导致本地端向远程端发送单个 HTTP 请求,表示单个 WebDriver 命令。
在本地端收到相应的 HTTP 响应之前,程序不会继续。例如,在最初的 browser.go
调用中,远程端只会在浏览器完成请求的页面的加载后发送其响应。
该程序在网络上传输的 HTTP 流量如下所示(为了简洁起见,省略了一些无关紧要的细节)。
POST /session/25bc4b8a-c96e-4e61-9f2d-19c021a6a6a4/url HTTP/1.1 Content-Length: 43 {"url": "http://localhost:8000/index.html"}
此时,浏览器执行网络操作以导航到请求的 URL,http://localhost:8000/index.html
。一旦该页面完成加载,远程端就会将以下响应发送回自动化客户端。
HTTP/1.1 200 OK content-type: application/json; charset=utf-8 content-length: 14 {"value":null}
接下来是查找具有类 test
的元素的请求。
POST /session/25bc4b8a-c96e-4e61-9f2d-19c021a6a6a4/elements HTTP/1.1 Content-Length: 43 {"using": "css selector", "value": ".test"}
HTTP/1.1 200 OK content-type: application/json; charset=utf-8 content-length: 90 {"value":[{"element-6066-11e4-a52e-4f735466cecf":"0d861ba8-6901-46ef-9a78-0921c0d6bb5a"}]}
最后是获取元素标签名称的请求。
GET /session/25bc4b8a-c96e-4e61-9f2d-19c021a6a6a4/element/0d861ba8-6901-46ef-9a78-0921c0d6bb5a/name HTTP/1.1
HTTP/1.1 200 OK content-type: application/json; charset=utf-8 content-length: 15 {"value":"div"}
即使这三行代码涉及大量的网络操作,但控制流也易于理解,并且易于用大量常见编程语言表达。这与浏览器本身内部的情况大不相同,在那里,一个看似简单的操作(如加载一个页面)包含大量异步步骤。
远程端处理所有这些复杂性,使得编写自动化客户端变得更加容易。
灵活性
在上面的简单模型中,本地端直接与控制浏览器的驱动程序二进制文件通信。但在实际测试部署场景中,情况可能更加复杂;可以在本地端和驱动程序之间部署任意的 HTTP 中间件。
这的一种常见应用是提供配置功能。使用像 Selenium Grid 这样的中间件,单个 WebDriver HTTP 端点可以面对大量的操作系统和浏览器组合,将每个测试的命令代理到请求的机器上。
HTTP 的易于理解的语义,加上丰富的现有工具,使得这种设置相对容易在规模上构建和部署,即使是在像互联网这样不受信任的、可能延迟较高的网络上也是如此。
这对像 SauceLabs 和 BrowserStack 这样的服务很重要,它们在远程服务器上运行自动化。
基于 HTTP 的 WebDriver 的局限性
HTTP 的同步命令/响应模型给 WebDriver 带来了一些限制。由于浏览器只能响应命令,因此很难模拟可能在特定请求之外的浏览器中发生的事情。
一个明显的例子是警报。警报可以随时出现,因此每个 WebDriver 命令都必须在运行之前专门检查是否存在警报。
类似的问题也发生在日志记录中;理想的 API 会在日志事件生成时立即发送它们,但在基于 HTTP 的 WebDriver 中这是不可能的。相反,日志记录 API 需要在浏览器端进行缓冲,并且客户端必须接受它可能无法收到所有日志消息。
关于标准化一个糟糕的、不可靠的 API 的担忧意味着,尽管日志记录功能是用户常见请求,但它还没有被纳入 WebDriver 的 W3C 规范。
WebDriver 采用 HTTP 模型的原因之一是编程模型的简单性。使用完全阻塞的 API,人们可以轻松地使用 21 世纪初的主流语言特性编写 WebDriver 客户端。
从那时起,许多编程语言都获得了对处理事件和异步控制流的一流支持。这意味着,最初的 WebDriver 协议中的一些基本假设(例如,异步、事件驱动的代码太难写)不再成立了。
DevTools 协议
除了通过 WebDriver 进行自动化之外,现代浏览器还提供远程访问以使用浏览器的 DevTools。这对于难以在页面本身运行的同一台机器上调试问题的情况至关重要,例如只在移动设备上出现的问题。
不同的浏览器提供不同的 DevTools 功能,这些功能通常需要引擎中的显式支持,并公开对 Web 内容不可见的实现细节。因此,每个浏览器引擎都有一个独特的 DevTools 协议,根据它们特定的要求。
在 DevTools 中,有一个核心要求是 UI 必须响应浏览器引擎发出的事件。例如,在日志控制台消息和网络请求出现时,实时更新它们,以便用户可以跟踪进度。
这意味着 DevTools 协议不使用 HTTP 的命令/响应范式。相反,它们使用双向协议,其中消息可能来自客户端或浏览器。这允许 DevTools 实时更新,响应浏览器发生的更改。
远程自动化不是 DevTools 的核心用例。在一种情况下常见的一些操作在另一种情况下很少见。例如,客户端发起的导航几乎存在于所有自动化测试中,但在 DevTools 中很少见。
尽管如此,调试时所需的低级控制意味着可以在 DevTools 协议功能集之上编写许多自动化功能。事实上,在像 Chrome 这样的某些浏览器中,用于弥合 WebDriver 二进制文件和浏览器本身之间差距的浏览器内部消息格式实际上是 DevTools 协议。
这不可避免地引出了一个问题,即是否可以在 DevTools 协议之上直接构建自动化。随着语言提供更好的对异步控制流的支持,以及现代 Web 应用程序要求更多低级控制以进行测试,像 Google 的 Puppeteer 这样的库已经利用 DevTools 协议并在其之上构建了自动化特定的客户端库。
这些库支持高级功能,例如网络请求拦截,这些功能很难在基于 HTTP 的 WebDriver 之上构建。典型的基于 promise 的 API 也感觉更像是现代前端编程,这使得这些工具在 Web 开发者中很受欢迎。
即使是主要基于 WebDriver 的工具也添加了无法仅通过 WebDriver 实现的附加功能。例如,Selenium 4 中的一些新功能,如访问控制台日志和更好地支持 HTTP 身份验证,需要双向通信,并且最初只支持可以讲 Chrome 的 DevTools 协议的浏览器。
DevTools 困难
虽然将 DevTools 用于自动化在功能集方面很有吸引力,但也存在许多问题。
DevTools 协议是浏览器特定的,可以公开许多不在 Web 平台中的内部状态。这意味着使用 DevTools 功能进行自动化的库通常绑定到特定的渲染引擎。
它们也受制于这些引擎的更改;与引擎内部的紧密耦合意味着 DevTools 协议通常对稳定性提供非常有限的保证。
对于 DevTools 本身来说,这不是一个大问题;同一个团队通常拥有前端和后端,因此任何重构只需要同时更新客户端和服务器,跨版本兼容性不是一个严重的问题。但对于自动化来说,这对客户端库开发人员和测试编写者都造成了重大负担。
使用 WebDriver,单个客户端可以与任何受支持的浏览器版本一起使用。使用基于 DevTools 的自动化,可能需要为每个浏览器版本创建一个新的客户端。例如,Puppeteer 就是这种情况,每个 Puppeteer 版本都绑定到特定版本的 Chromium。
DevTools 协议是特定于浏览器的这一事实,使得将它们用作跨浏览器工具的基础非常具有挑战性。一些自动化客户端,如 Cypress 和微软的 Playwright,在这方面做出了巨大的努力,放弃了 WebDriver,但仍然支持多个浏览器。
它们通过组合使用现有的 DevTools 协议和通过对底层浏览器代码进行修补或通过 WebExtensions 实现的自定义协议,提供了 WebDriver 中无法实现的功能,同时支持多个浏览器引擎。
需要自动化库维护如此大量的代码,并将库置于浏览器引擎更新的“跑步机”上,使得维护变得困难,并让库作者无暇顾及他们核心自动化功能的开发。
总结和下一步
正如我们所看到的,web 应用程序测试生态系统正在变得碎片化。大多数跨浏览器测试使用 WebDriver;这是一项所有主要浏览器引擎都支持的 W3C 规范。
然而,WebDriver 基于 HTTP 的协议的局限性意味着自动化库越来越多地选择使用特定于浏览器的 DevTools 协议来实现高级功能,从而在这样做时放弃了跨浏览器支持。
测试编写者不应该被迫在访问功能和特定于浏览器的工具之间进行选择。客户端作者也不应该被迫跟上浏览器引擎开发的快速步伐。
在我们的下一篇文章中,我们将描述 Mozilla 在将以前仅限于 Chromium 的测试工具引入 Firefox 时所做的工作。
致谢
感谢 Tantek Çelik、Karl Dubost、Jan Odvarko、Devin Reams、Maire Reavy、Henrik Skupin 和 Mike Taylor 提供宝贵的反馈和建议。
关于 Maja Frydrychowicz
关于 James Graham
专注于维护健康的开放网络的软件工程师。Web 平台测试核心团队成员。
一条评论