一些 UI 测试问题以及 Cypress 的方法
由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!
请注意:所有现代前端测试工具(Playwright、Storybook、Cypress、TestCafé)都具有类似的功能和用户界面工具,因此本文档的内容适用于所有这些工具,而不仅仅是 Cypress。
为什么测试 Web 应用程序如此困难?为什么通用的浏览器自动化工具无法很好地满足 UI/E2E 测试需求?Cypress 的优势何在?
仅仅进行一般性的功能比较不足以了解 UI 测试的主要痛点以及 Cypress 如何消除这些痛点。
我正在 GitHub 上开发一个大型的UI 测试最佳实践项目,分享这篇文章是为了传播它并获得直接反馈。
本文是我在 2020 年 3 月 27 日于伦敦举行的CityJSConf大会上演讲的基础。
测试前端应用程序会带来一些“传统”测试所没有的挑战:你需要配置一个真实的浏览器。浏览器本身就是重量级应用程序,你需要启动它们,通过专门编写的库来管理它们,利用一些 API 来自动化用户会执行的各种交互操作,然后检查前端应用程序的状态(本质上是它显示的内容)是否与预期一致。
这个过程及其涉及的步骤正是UI测试的难点所在。主要问题包括:
- 一切都是异步的:用户模拟的交互是异步的,用户界面是异步的,浏览器是异步的,你用来协调和与浏览器通信的工具也是异步的。
await page.goto(url)
await page.click('[data-test="contact-us-button"]')
await expect(page).toMatch('Contact Us')
而且,一旦你需要更复杂的东西,等待一切就会导致你深入管理承诺和递归承诺。
-
你需要自动化用户流程:因此,你需要重现用户流程、检查自动执行的用户流程、调试失败的(以及自动且速度极快的)用户流程。
想象一下,你和一位同事并肩工作,他遇到了一个问题,你让他创建一些东西,以便你可以直接使用他的浏览器开发者工具检查问题,但他却在你检查问题时没有停下来点击/输入。这就是你在 UI 测试中遇到的类似情况。暂停/停止正在运行的流程非常困难,你可能需要多次重新启动相同的测试。 -
在 Web 应用中,有很多因素会影响元素的交互性:元素的内部状态、标记属性、视觉外观、其他元素的外观等等。有些很容易发现(例如“disabled”属性),但有些则不然(例如另一个元素的 z-index 值更高)。更普遍地说,调试这些原因很困难,因为需要仔细检查元素本身、整个页面、用于自动化交互的工具等等。
自动化和测试前端应用程序很困难,但有些工具并不能减轻痛苦,而有些工具却能赋予你超能力,继续吧!
常用工具
要实现前端应用程序的自动化测试,您需要两种不同的工具:
-
测试运行器:负责执行测试本身。
-
浏览器自动化工具:一种提供一些 API 以便与特意启动的浏览器进行交互的工具。
这两个工具是独立的,你选择的测试运行器(例如 Jest)在终端中运行(并向你提供所有测试反馈),而第二个测试运行器(Selenium 或 Puppeteer)打开浏览器,执行测试中编写的命令,并返回结果。
这两个工具是分离的,这让很多事情变得复杂!浏览器中的操作速度非常快!你可以减慢它们的速度,但无法暂停或停止它们!或者更准确地说,无法以交互方式暂停或停止……因为你当然可以在代码编辑器和浏览器之间来回切换,修改测试代码,注释掉你想检查的步骤之后的所有内容,然后重新启动测试并查看发生了什么。但这并非理想的工作流程。而且由于测试本身就是一个小程序,你知道你需要重复这个步骤很多次……
按照上述方式运行测试时,还会出现另一个问题:通常情况下,您需要登录到终端(测试运行器所在的终端),而实际操作却在浏览器中进行。如何才能将它们关联起来呢?您是否需要在终端和浏览器控制台中都添加时间戳日志?或者您是否需要在前端应用程序上方添加一个固定的div元素来显示正在运行的测试名称?将浏览器中的操作与您通过终端执行的操作(或日志)关联起来也并非易事。
最后但同样重要的是:在终端调试测试时,您调试的并非真实的 DOM 元素,而是序列化/引用的元素。终端和浏览器之间没有任何双向交互,因此您无法像以往那样使用浏览器开发者工具。
相信我,通过这种方式,理解测试失败的原因或者浏览器为何没有按照预期运行真的很难。但你必须在测试执行过程的三个不同阶段都面对这个问题:
-
1:当你最初编写测试时
-
2:测试失败,无法向生产环境发送任何内容。
-
3:由于规格变更,您需要更新它们。
步骤 1 和步骤 3 非常相似,步骤 3 可能更快,但步骤 1 可能会很累。如果使用的工具不好用,步骤 2 会让你彻底讨厌 UI 测试……
测试运行目的
请停下来思考一下,上述工具试图实现什么目标,首先从测试运行器开始。
测试运行器是为管理单元测试而设计的。当然,您可以根据自己的需要使用/集成它们,但它们本质上是为超快速(且并行化)的小函数调用而设计的。它们没有类似浏览器的开发者工具,但主要问题在于测试超时。每个测试都有一个超时时间,这完全合理。正是由于超时机制,如果测试耗时过长,测试运行器就会终止它。
但是,当测试超时与 UI 测试需求结合起来时会发生什么呢?众所周知,用户流程可能会持续很长时间。原因有很多:
-
交互过程本身可能非常漫长,涉及数十次点击、打字、计算、等待等等。
-
有很多因素(从持续时间的角度来看)是完全无法控制的:尤其是 XHR 请求!你无法预知 Docker 容器(或测试服务器)需要多长时间才能响应。而且,如果后端没有容器化,你还得面对网络速度慢的问题。
这些例子展示了 UI 测试的不可预测性。解决方案看似简单:增加测试超时时间!但这却是最糟糕的方案,因为它根本行不通,原因如下:
-
测试超时就像一把断头台,能在出现问题时帮你节省大量时间。如果你把超时时间设为一分钟,那么一旦某个测试没有按预期运行,你就得再等一分钟(整整 60 秒!)。测试时间过长是开发者讨厌测试的主要原因之一,因为这样一来,流水线就会永远运行下去。然而,在某些特定情况下,你无法确定 60 秒是否足够……想想看,如果服务器运行缓慢,再加上网络问题,AWS Lambda 函数的唤醒时间会有多长……
-
调试过程呢?请记住,当测试因超时而被终止时,自动化浏览器会自动关闭……
最后但同样重要的是,请记住您需要进行与 DOM 相关的断言。在 UI 测试中,您不会处理对象、数组和基本类型,而是主要管理 DOM 元素。类似“我期望元素等于……”这样的断言在 UI 测试中不起作用,而它在单元测试中显然是有效的。这个问题通常可以通过外部插件来解决。
浏览器自动化工具用途
Selenium 和 Puppeteer 旨在提供简单易用、无需任何特殊技巧的 UI 自动化体验。它们并非用于测试 UI,而是专门用于自动化用户交互。自动化和测试在某些方面有所重叠,但二者并不相同。两者都会尝试判断按钮是否可点击并执行点击操作,但前者会失败,而后者则会告诉你失败的原因。例如,前者会告诉你某个元素不在页面上,而后者则会告诉你该元素不在页面上是因为之前的 XHR 请求失败。
我们过去习惯于将测试运行器与浏览器自动化工具结合起来,并努力发挥它们各自的优势,但却饱受两个未集成且不同的工具无法提供的功能的困扰。
再说回测试(以及被测应用)的可调试性:为了减慢速度/调试/暂停/停止/使其正常运行等等,你需要频繁地让测试“休眠”。这是一种常见的做法,既因为它能在短期内解决问题,有时也是因为你别无选择(请阅读我之前关于“ Await,不要让你的端到端测试休眠”的文章)。不幸的是,添加过多的“休眠”步骤会让测试变得越来越糟糕,速度越来越慢。正如我之前所写:测试速度慢是导致开发者讨厌 UI 测试的最常见缺陷之一。
更多内容:测试失败时会发生什么?在了解如何修复 bug 之前,您可以做些什么来理解问题?如果您足够幸运,在本地发现了出错的测试,那么您的麻烦就有限了。但如果测试在流水线中失败,如果没有用户界面,您如何知道发生了什么?您是否添加过 Parachute Automate 的屏幕截图?还有什么比屏幕截图更有说服力的吗?很遗憾,没有……
您甚至需要利用第三方调试工具(React DevTools、Vue DevTools 等),但它们在受控浏览器上的安装过程并不方便。
最后但同样重要的是:模拟服务器并断言 XHR 请求可以被视为测试实现细节……但我认为并非如此,原因有二:
-
谈到黑盒测试,我们指的是一种(良好的)实践,即避免测试某个功能如何运作,而只关注它实际执行的操作。应用于前端应用程序时,这意味着只测试应用程序向用户公开的功能,而不是应用程序如何公开这些功能(无论它使用 React 还是 Vue.js,无论它将数据保存到 localStorage 还是 sessionStorage 都无关紧要)。这同样适用于客户端/服务器通信,但要弄清楚某个功能未执行是因为错误的 XHR 请求可能很困难(尤其是在以无头模式运行自动化浏览器时)。然而,通过断言请求负载、响应负载、响应状态等信息来发现问题却至关重要(始终要关注测试在多大程度上能够帮助你识别失败的原因)。
-
如果您使用 Pact 或类似工具测试客户端/服务器契约,则无需执行此操作,但您的工作流程中是否有此类测试?
-
如果你是一名前端开发人员,你肯定知道你不可能总是在后端开发人员完成工作后才开始工作。但如果他们提供了完整的 JSON 响应,那么通过模拟后端,你就可以完成所有前端编码工作,只需在前端与后端集成后检查一切是否按预期运行即可。这关乎效率。
隐式测试的挑战
上述考虑引出了另一个问题:测试代码应该尽可能简单。测试可以让你检查一切是否按预期运行,但它们毕竟也是小程序。因此,你需要长期维护它们。而且,由于你需要在一段时间内理解它们(如果需要花费数小时才能理解测试的原理和工作方式是不切实际的,测试应该帮助你,而不是像糟糕的代码那样让你的工作更加复杂),所以测试代码不应该过于复杂(请参阅我的文章《软件测试作为文档工具》)。
但是,那些并非为UI测试这类复杂任务而设计的工具,并不能帮助你编写简单的测试代码。你的测试工作又会变得更加艰难……因此,你不得不花费大量时间调试失败的测试,而不是去理解前端应用程序究竟哪里出了问题(假设确实存在问题……)。最终导致测试结果的可信度降低……
柏树前来救援
别担心,我并非故意让你难过才报道这件事的😉,只是想让你意识到,你需要的不是各种通用工具的混搭,而是专门设计的工具!我首先想到的是Cypress和TestCafé。这两款工具都非常出色,因为它们的目标只有一个:重塑(或者说改进?)UI 测试领域。
我主要关注 Cypress,稍后再进行比较。Cypress
是如何解决上述所有问题的呢?首先……
Cypress 拥有用户界面。
是的,Cypress 是通过终端启动的,但它是通过用户界面使用的!而且用户界面会与应用程序并排显示!看看这个预览吧。
命令日志用户界面(左侧)与您的前端应用程序(右侧)并行运行。
这是什么意思?命令日志用户界面的主要功能有哪些?
- 你可以直接了解 Cypress 的运行情况。每次你通过命令(例如 `cy.click`、`cy.type` 等)让 Cypress 与页面交互时,Cypress 都会在测试运行器中添加日志。这种详细的自动日志记录在编写测试和调试测试时都非常有用。它能显著提高你的工作效率,因为它既是自动化的,又与你的应用程序并排显示。
但是,正如我之前所说,缺乏追溯调试能力在编写 UI 测试时是一个很大的缺陷……让我来介绍一下……
- 交互式时间旅行:不确定应用是如何执行到某个特定命令的,或者测试为何失败?不妨查看一下之前步骤的 UI 界面。这就是命令日志交互式功能的用武之地!您可以将鼠标悬停在各个已记录的步骤上,查看应用在特定步骤的界面!当然,您也可以锁定某个步骤并检查 DOM,查看应用在该步骤前后的界面变化等等。这又是一个救星功能,无论是在初次尝试(如果您不熟悉测试工具,调试测试简直是一场噩梦)还是在日常测试工作中,它都能派上用场。它让测试检查变得如此便捷,以至于您完全忘记了没有它时的测试是怎样的。观看演示视频,了解它的实际应用。
其他命令日志实用程序包括:
-
命令的详细日志:点击命令会在浏览器开发者工具中显示更详细的日志。
-
断言检查:点击断言后,浏览器开发者工具中会同时显示预期值和结果。无需重新运行测试并启用更详细的日志记录。
-
如果你监听 XHR 呼叫,命令日志会显示监听/拦截的呼叫记录以及呼叫次数。
…以及更多功能,请查看Cypress 官方文档了解其具体功能。
柏树命令
命令默认是异步的,请看下面的代码片段。
cy.visit(url)
cy.click('[data-test="contact-us-button"]')
cy.contains('Contact Us').should('be.visible')
你注意到有任何 await 语句吗?没有,原因很简单:既然 UI 中的所有操作都需要 await,为什么还要由你来管理 await 呢?Cypress 会为你“等待”,这意味着,如果你尝试与某个 DOM 元素交互时它还没有准备好,没关系!Cypress 会重试(默认重试 4 秒),直到可以与该元素交互为止(以用户的方式,即元素可见、未被禁用、未被遮挡等)。因此,你可以完全避免前端固有的异步性问题!
上述特性还有另一个好处:你还记得不太好用的测试超时机制吗?现在,彻底告别它吧!在 Cypress 中,测试没有超时限制!你无需猜测(并根据需要不断调整)测试时长,每个命令都有自己的超时时间!如果出现问题,测试会立即失败!如果测试顺利进行,也不会面临超时的威胁!
最后但同样重要的是:DOM 相关命令会按照您需要的方式报告DOM 相关错误。请看以下示例:
用户无法在输入框中输入内容的原因很明显。Cypress 并非唯一一款能够模拟用户操作的命令工具,但它的语音识别错误非常罕见。
测试质量
开发人员在测试过程中会犯很多常见的错误。有些错误可以忽略不计,但有些则不然。Cypress 如何帮助你避免这些错误呢?
-
重置状态:测试之间不共享状态,因为 cookie、localStorage 等都会在每次测试前重置。当然,你可以创建一些巧妙的命令来保持测试的独立性(共享状态的真正问题在于测试的独立性,可以参考我课程中的示例),但你不能跳过重置步骤。相信我,这绝对是个好方法 😉
-
如果断言失败,就不能再进行测试,因为这样就排除了恢复测试的可能性。你需要让测试更加稳定,即使有时这看起来很困难。这是一个明智的选择,否则,你就可以被允许编写糟糕的测试了。
-
有了大量的等待辅助功能:重试机制和自动等待功能简直是救星,让你可以专注于应用和测试本身,而不是等待元素之类的琐事。Cypress 允许你等待 DOM 元素、XHR 请求和页面加载,并且会根据需要调整超时时间(XHR 请求或页面加载的时间可能比输入元素出现的时间更长),而无需使用固定时间的睡眠(再次提醒,请阅读我的文章《Await,不要让你的端到端测试进入睡眠状态》)。
如果你需要编写自定义等待,我的cy.waitUntil 插件正是你需要的 😉
生产率
Cypress 在另一个非常重要的方面也胜出:生产力。每个人都问我“测试会拖慢多少工作速度?”。我的标准回答是,如果我编写测试,那么编写同样的代码需要多花 30% 到 40% 的时间。但是,编写完整的测试套件和自动化测试之间存在着区别。我专门写了一篇文章来探讨这个话题:《前端生产力提升:Cypress 作为你的主要开发浏览器》。
我不会在这里重复所有内容,但请记住:
-
使用 Cypress 可以轻松实现开发流程自动化。保存应用或测试代码时,测试会自动重新启动,这样您就无需手动点击/输入相同的步骤来检查正在开发的新功能。
-
Cypress 利用持久化的 Chrome 用户,您可以在 Cypress 控制的浏览器中安装您选择的开发者工具并使用它们。
-
Cypress 允许你完全控制服务器,既可以监视、等待和断言 XHR 请求,也可以使用静态 JSON 响应而不是服务器响应。后者能让你的开发效率提升十倍,毕竟,你是一名前端开发人员,你需要找到在没有真正后端的情况下工作的方法(除非你运气好到可以在本地机器上运行)。
-
您可以完全访问应用程序所属的窗口对象。这对于提高工作效率、使用前端应用程序公开的功能等来说,是另一项重要的优势。
调试
我在上面解释了为什么如果没有一些专门的功能,调试测试会是一场噩梦。失败的测试调试有两种类型:
-
在编写测试时
-
当 CI/CD 流水线中的测试失败时
Cypress 提供了两种绝佳的解决方案:
-
播放/暂停功能**:无论通过编程还是用户界面,您都可以暂停和恢复测试。而且,它甚至提供分步导航,就像您在代码中设置断点并逐步处理一样。用过两次播放/暂停功能后,您就会发现它简直是必备工具😊
播放/暂停和时间旅行功能带来绝佳体验,让您彻底告别耗时的调试烦恼。 -
自动截图和录像:如果测试失败,Cypress 会保存测试最后一步的截图。有时,最后一步的截图可以帮助你了解发生了什么(尤其是在添加了大量语句的情况下,你可以从中了解如果没有清晰的步骤说明会面临哪些风险),但如果截图无法提供太多帮助……Cypress 会录制整个测试过程的视频,包括测试运行器的用户界面。有时,自动录制功能让我能够以最简单的方式发现持续集成 (CI) 相关的问题。
常问问题
我向大家介绍了 Cypress 这款完美的工具,现在我预料他们会问我一些常见问题:
-
Cypress 是免费的吗?
是的,它是免费的,开源的,采用 MIT 许可证。只有当您需要使用其Dashboard 服务时才需要付费。简单来说:如果您想让 Cypress 托管您的测试视频,则需要付费;否则,一切都是免费的。 -
Cypress除了Chrome系列浏览器之外,还支持其他浏览器吗?
除了Chrome和Edge 80+之外,Cypress也支持Firefox。 -
我提到了TestCafé,它们的主要区别是什么?
-
TestCafé 没有类似测试运行器 UI 的功能,恕我直言,这是一个很大的缺失。
-
TestCafé 会一直等待 DOM 元素超时,而 Cypress 则会等待相同的超时时间。因此,使用 TestCafé 时,您需要手动调整等待时间以避免测试运行时间过长;而使用 Cypress 则无需担心这个问题。
-
TestCafé 并不完全支持 XHR 请求检查,这一点值得商榷,但我认为这对于实现高可靠性的测试和有用的错误报告至关重要。
-
TestCafé 支持所有主流浏览器!这是一项独特的功能,Cypress 并不支持所有浏览器,而且未来也不会支持移动浏览器。请注意,跨浏览器兼容性的需求通常被高估了,但如果您确实需要这项功能,TestCafé 绝对是您的理想之选
。附:如果您是一位经验丰富的 TestCafé 开发者,我非常希望听到您的经验分享,以丰富本文内容并提升我对 TestCafé 的整体认知 😊 -
Cypress 有缺陷吗?当然有!它存在一个历史遗留的 window.fetch 问题,迫使你使用 Axios 或添加变通方案;而且由于你的应用运行在 iframe 中,你可能还需要一些额外的步骤来管理 OAuth。尽管如此,它仍然是最受欢迎的 UI 测试工具之一。
-
更笼统地说:请记住,我们讨论的是用户界面测试,而 Cypress 在这方面尤其出色。如果您只是需要自动化浏览器(用于数据抓取或其他用途),请不要使用它!
结论
让我回顾一下我上面列出的问题和解决方案:
-
前端测试的所有操作都是异步的,Cypress 几乎可以透明地管理这一切。
-
逐步调试:Cypress 的时间旅行和播放/暂停功能是你最好的朋友
-
Cypress 在出现故障时会给出清晰的错误信息。
-
调试变得轻松便捷,得益于并排运行的测试/应用程序,识别缺陷不再是噩梦。
-
故障发生时自动截屏和录像
-
Cypress 测试没有超时限制,Cypress 命令有超时限制。
-
Cypress 让您能够以最简单的方式完全无需后端即可工作。
-
Cypress 拥有许多可以提高您工作效率的功能。
-
Cypress 的设计初衷只有一个:让 UI 测试变得轻松 😊
我以 Cypress 网站上的一段话作为文章结尾😊
文章来源:https://dev.to/noriste/some-ui-testing-problems-and-the-cypress-way-1167网络不断发展
,测试方式也随之演变。

