编写 Cypress 测试的 1,000 英尺概述 #frontend@twiliosendgrid

已发表: 2020-10-03

在 Twilio SendGrid,我们编写了数百个赛普拉斯端到端 (E2E) 测试,并且随着不同 Web 应用程序和团队发布新功能,我们继续编写更多测试。 这些测试涵盖了整个堆栈,验证客户在我们的应用程序中推送新代码更改后会经历的最常见用例是否仍然有效。

如果您想先退后一步,阅读更多关于如何总体上考虑 E2E 测试的信息,请随时查看此博客文章,并在您准备好后返回此内容。 这篇博文并不要求您成为 E2E 测试方面的专家,但它有助于保持正确的心态,因为您会明白为什么我们在测试中以某种方式做事。 如果您正在寻找向您介绍赛普拉斯测试的分步教程,我们建议您查看赛普拉斯文档 在这篇博文中,我们假设您之前可能已经看过或编写过许多 Cypress 测试,并且很想知道其他人如何为自己的应用程序编写 Cypress 测试。

在编写了大量赛普拉斯测试之后,您将开始注意到自己使用类似的赛普拉斯函数、断言和模式来完成您需要的事情。 我们将向您展示我们使用或使用赛普拉斯之前使用或完成的最常见的部分和策略,以针对单独的环境(例如 dev 或 staging)编写测试。 我们希望这 1,000 英尺长的关于我们如何编写赛普拉斯测试的概述能够为您提供与您自己的比较的想法,并帮助您改进处理赛普拉斯测试的方式。

大纲:

  1. 赛普拉斯 API 综述
  2. 与元素交互
  3. 断言元素
  4. 处理 API 和服务
  5. 使用 cy.request(...) 发出 HTTP 请求
  6. 使用 cy.task() 创建可重用插件
  7. 使用 cy.server() 和 cy.route() 模拟网络请求
  8. 自定义命令
  9. 关于页面对象
  10. 选择不使用 window.Cypress 检查运行客户端代码
  11. 处理 iframe
  12. 跨测试环境标准化

赛普拉斯 API 综述

让我们从最常用于 Cypress API 的部分开始。

选择元素

选择 DOM 元素的方法有很多种,但您可以通过这些 Cypress 命令完成大部分需要执行的操作,并且通常可以在这些命令之后链接更多操作和断言。

  • 使用cy.get(“[data-hook='someSelector']”)cy.find(“.selector”)获取基于某些 CSS 选择器的元素。
  • 根据某些文本选择元素,例如cy.contains(“someText”)或使用包含某些文本的特定选择器获取元素,例如cy.contains(“.selector”, “someText”)
  • 让父元素在“内部”查看,因此您未来的所有查询都将作用于父元素的子元素,例如cy.get(“.selector”).within(() => { cy.get(“.child”) })
  • 查找元素列表并查看“每个”以执行更多查询和断言,例如cy.get(“tr”).each(($tableRow) => { cy.wrap($tableRow).find('td').eq(1).should(“contain”, “someText” })
  • 有时,元素可能不在页面的视图中,因此您需要先将元素滚动到视图中,例如cy.get(“.buttonFarBelow”).scrollIntoView()
  • 有时您需要比默认命令超时更长的超时,因此您可以选择添加{ timeout: timeoutInMs }cy.get(“.someElement”, { timeout: 10000 })

与元素交互

这些是我们在赛普拉斯测试中发现的最常用的交互。 有时,您需要在这些函数调用中添加{ force: true }属性以绕过对元素的一些检查。 当元素以某种方式被覆盖或从外部库中派生出来时,通常会发生这种情况,而您对其呈现元素的方式没有太多控制权。

  • 我们需要点击很多东西,比如模态框、表格等中的按钮,所以我们执行cy.get(“.button”).click()之类的操作。
  • 在我们的 Web 应用程序中随处可见用于填写用户详细信息和其他数据字段的表单。 我们使用cy.get(“input”).type(“somekeyboardtyping”)这些输入,我们可能需要先清除输入的一些默认值,例如cy.get(“input”).clear().type(“somenewinput”) 当您执行cy.get(“input”).type(“text{enter}”)时,还有一些很酷的方法可以输入其他键,例如{enter}作为 Enter 键。
  • 我们可以与cy.get(“select”).select(“value”)等选择选项和cy.get(“.checkbox”).check()等复选框进行交互。

断言元素

这些是您可以在赛普拉斯测试中使用的典型断言,以确定页面上是否存在内容正确的内容。

  • 要检查页面上是否显示内容,您可以在cy.get(“.selector”).should(“be.visible”)cy.get(“.selector”).should(“not.be.visible”)
  • 要确定 DOM 元素是否存在于标记中的某处并且您不一定关心元素是否可见,您可以使用cy.get(“.element”).should(“exist”)cy.get(“.element”).should(“not.exist”)
  • 要查看一个元素是否包含某些文本,您可以在cy.get(“button”).should(“contain”, “someText”)cy.get(“button”).should(“not.contain”, “someText”)
  • 要验证输入或按钮是否被禁用或启用,您可以这样断言: cy.get(“button”).should(“be.disabled”)
  • 要断言是否检查了某些内容,您可以进行测试,例如cy.get(“.checkbox”).should(“be.checked”)
  • 您通常可以依赖更具体的文本和可见性检查,但有时您必须依赖类检查,例如cy.get(“element”).should(“have.class”, “class-name”) 还有其他类似的方法可以使用.should(“have.attr”, “attribute”)来测试属性。
  • 将断言链接在一起通常很有用,例如cy.get(“div”).should(“be.visible”).and(“contain”, “text”)

处理 API 和服务

在处理您自己的与电子邮件相关的 API 和服务时,您可以使用cy.request(...)向带有 auth 标头的后端端点发出 HTTP 请求。 另一种选择是您可以构建可以从任何规范文件调用的cy.task(...)插件,以涵盖其他功能,这些功能可以在具有其他库的 Node 服务器中最好地处理,例如连接到电子邮件收件箱并查找在返回一些值供测试使用之前,匹配电子邮件或对某些 API 调用的响应和轮询有更多控制。

使用 cy.request(...) 发出 HTTP 请求

您可以使用cy.request()向后端 API 发出 HTTP 请求,以在测试用例运行之前设置或删除数据。 您通常会传入端点 URL、HTTP 方法(例如“GET”或“POST”)、标头,有时还会传入要发送到后端 API 的请求正文。 然后,您可以将其与.then((response) => { })链接,以通过“status”和“body”等属性访问网络响应。 此处演示了进行cy.request()调用的示例。

有时,您可能不关心cy.request(...)在测试运行前的清理过程中是否会失败并显示 4xx 或 5xx 状态码。 您可以选择忽略失败状态代码的一种情况是,当您的测试发出 GET 请求以检查项目是否仍然存在并已被删除时。 该项目可能已被清理,GET 请求将失败并显示 404 not found 状态代码。 在这种情况下,您将设置另一个选项failOnStatusCode: false ,这样您的赛普拉斯测试在运行测试步骤之前就不会失败。

使用 cy.task() 创建可重用插件

当我们想要更灵活地控制可重用功能以通过 Node 服务器与另一个服务(例如电子邮件收件箱提供商)通信时(我们将在以后的博客文章中介绍这个示例),我们希望提供我们自己的额外功能和对 API 调用的自定义响应让我们链接并应用到我们的赛普拉斯测试中。 或者,我们喜欢在 Node 服务器中运行一些其他代码——我们经常为它构建一个cy.task()插件。 我们在模块文件中创建插件函数并将它们导入到plugins/index.ts中,我们在其中使用运行函数所需的参数定义任务插件,如下所示。

这些插件可以在您的规范文件中的任何位置使用cy.task(“pluginName”, { ...args })调用,并且您可以期望发生相同的功能。 然而,如果您使用cy.request() ,除非您将这些调用本身包装在页面对象或帮助文件中以便在任何地方导入,否则您的可重用性会降低。

另一个需要注意的是,由于插件任务代码是在 Node 服务器中运行的,因此您不能在Cypress.env(“apiHost”)cy.getCookie('auth_token')等这些函数中调用常用的 Cypress 命令。 除了请求正文需要与您的后端 API 对话时,您还可以将诸如身份验证令牌字符串或后端 API 主机之类的内容传递给插件函数的参数对象。

使用 cy.server() 和 cy.route() 模拟网络请求

对于需要难以重现的数据的赛普拉斯测试(例如页面上重要 UI 状态的变化或处理较慢的 API 调用),赛普拉斯需要考虑的一项功能是排除网络请求。 如果您使用 vanilla XMLHttpRequest、axios 库或 jQuery AJAX,这适用于基于 XmlHttpRequest (XHR) 的请求。 然后,您将使用cy.server()cy.route()来侦听路由以模拟您想要的任何状态的响应。 这是一个例子:

另一个用例是一起使用cy.server()cy.route()cy.wait()来侦听并等待网络请求完成,然后再执行下一步。 通常,在加载页面或在页面上执行某种操作后,直观的视觉提示将表明某事已完成或准备好让我们断言和采取行动。 对于没有这种可见提示的情况,您可以像这样显式地等待 API 调用完成。

一个大问题是,如果您使用 fetch 处理网络请求,您将无法模拟网络请求或等待它们以相同的方式完成。 您需要一种解决方法,用 XHR polyfill 替换普通的window.fetch ,并在按照这些问题中记录的那样运行测试之前执行一些设置和清理步骤 从赛普拉斯 4.9.0 开始,还有一个experimentalFetchPolyfill性的 FetchPolyfill 属性可能对您有用,但总的来说,我们仍在寻找更好的方法来处理我们应用程序中跨 fetch 和 XHR 使用的网络存根,而不会造成任何破坏。 从 Cypress 5.1.0 开始,有一个很有前途的新cy.route2()函数(参见 Cypress 文档)用于 XHR 和 fetch 请求的实验性网络存根,因此我们计划升级我们的 Cypress 版本并进行试验,看看是否它解决了我们的问题。

自定义命令

与 WebdriverIO 等库类似,您可以创建全局自定义命令,这些命令可以在您的规范文件中重复使用和链接,例如在测试用例运行之前通过 API 处理登录的自定义命令。 一旦您在诸如support/commands.ts之类的文件中开发它们,您就可以访问诸如cy.customCommand()cy.login()之类的函数。 编写用于登录的自定义命令如下所示。

关于页面对象

页面对象是选择器和函数的包装器,可帮助您与页面交互。 您不需要构建页面对象来编写测试,但最好考虑封装对 UI 的更改的方法。 您希望通过将事物组合在一起来使您的生活更轻松,以避免在多个文件中而不是在一个地方更新选择器和交互。

您可以定义一个具有通用功能的基本“页面”类,例如open()以供继承的页面类共享和扩展。 派生的页面类为选择器和其他辅助函数定义自己的 getter 函数,同时通过super.open()等调用重用基类的功能,如下所示。

选择不使用 window.Cypress 检查运行客户端代码

当我们使用 CSV 等自动下载文件测试流程时,下载通常会通过冻结测试运行来破坏我们的赛普拉斯测试。 作为一种折衷方案,我们主要想通过添加window.Cypress检查来测试用户是否可以达到正确的下载成功状态,而不是在我们的测试运行中实际下载文件。

在 Cypress 测试运行期间,会在浏览器中添加一个window.Cypress属性。 在您的客户端代码中,您可以选择检查窗口对象上是否没有 Cypress 属性,然后照常执行下载。 但是,如果它在赛普拉斯测试中运行,请不要实际下载该文件。 我们还利用检查window.Cypress属性来检查我们在 Web 应用程序中运行的 A/B 实验。 我们不想从 A/B 实验中添加更多的不稳定和非确定性行为,这可能会向我们的测试用户展示不同的体验,因此我们首先检查该属性不存在,然后运行下面突出显示的实验逻辑。

处理 iframe

使用 Cypress 处理 iframe 可能很困难,因为没有内置的 iframe 支持。 有一个正在运行的 [问题]( https://github.com/cypress-io/cypress/issues/136 ) 充满了处理单个 iframe 和嵌套 iframe 的解决方法,这取决于您当前的赛普拉斯版本可能会或可能不会起作用或您打算与之交互的 iframe。 对于我们的用例,我们需要一种方法来处理暂存环境中的 Zuora 计费 iframe,以验证电子邮件 API 和营销活动 API 升级流程。 我们的测试涉及在我们的应用程序中完成对新产品的升级之前填写示例账单信息。

我们创建了一个cy.iframe(iframeSelector)自定义命令来封装处理 iframe。 将选择器传递给 iframe 将检查 iframe 的正文内容,直到它不再为空,然后返回正文内容以将其与更多赛普拉斯命令链接,如下所示:

使用 TypeScript 时,您可以在index.d.ts文件中像这样输入 iframe 自定义命令:

为了完成我们测试的计费部分,我们使用 iframe 自定义命令来获取 Zuora iframe 的正文内容,然后选择 iframe 中的元素并直接更改它们的值。 我们以前在使用cy.find(...).type(...)和其他替代方法时遇到问题,但幸运的是,我们通过更改输入值找到了解决方法,并直接使用调用命令(即cy.get(selector).invoke('val', 'some value') 您还需要cypress.json配置文件中的”chromeWebSecurity”: false以允许您绕过任何跨域错误。 下面提供了其与填充选择器一起使用的示例片段:

跨测试环境标准化

在使用前面强调的最常见的断言、函数和方法使用 Cypress 编写测试之后,我们能够运行测试并让它们在一个环境中通过。 这是很好的第一步,但我们有多个环境来部署新代码并测试我们的更改。 每个环境都有自己的一组数据库、服务器和用户,但我们的赛普拉斯测试应该只编写一次以使用相同的一般步骤。

为了在最终将更改部署到生产环境之前针对多个测试环境(例如开发、测试和暂存)运行赛普拉斯测试,我们需要利用赛普拉斯添加环境变量和更改配置值的能力来支持这些用例。

要针对不同的前端环境运行测试

您需要更改通过 Cypress.config(“baseUrl”) 访问的“baseUrl” Cypress.config(“baseUrl”) ,以匹配那些 URL,例如https://staging.app.comhttps://testing.app.com 这会更改所有cy.visit(...)调用的基本 URL,以将其路径附加到。 有多种设置方法,例如在运行赛普拉斯命令之前设置CYPRESS_BASE_URL=<frontend_url>或设置--config baseUrl=<frontend_url>

要针对不同的后端环境运行测试

您需要知道 API 主机名,例如https://staging.api.comhttps://testing.api.com ,才能在“apiHost”等环境变量中设置并通过Cypress.env(“apiHost”) 这些将用于您的cy.request(...)调用以向某些路径(如“<apiHost>/some/endpoint”)发出 HTTP 请求,或作为另一个参数传递给您的cy.task(...)函数调用属性来知道要访问哪个后端。 这些经过身份验证的调用还需要知道您最有可能通过cy.getCookie(“auth_token”)存储在 localStorage 或 cookie 中的身份验证令牌。 确保此身份验证令牌最终作为“授权”标头的一部分或通过其他方式作为您请求的一部分传入。 有多种方法可以设置这些环境变量,例如直接在cypress.json文件中或在--env命令行选项中,您可以在 Cypress 文档中引用它们

要登录到不同的用户或使用不同的元数据:

既然您知道如何处理多个前端 URL 和后端 API 主机,那么您如何处理不同用户的登录呢? 您如何根据环境使用不同的元数据,例如与域、API 密钥和其他可能在测试环境中唯一的资源相关的内容?

让我们从创建另一个名为“testEnv”的环境变量开始,它可能的值是“testing”和“staging”,这样您就可以用它来判断要在测试中应用哪个环境的用户和元数据。 使用“testEnv”环境变量,您可以通过几种方式来解决这个问题。

您可以在fixtures文件夹下创建单独的“staging.json”、“testing.json”和其他环境JSON文件,并根据“testEnv”值如cy.fixture(`${testEnv}.json`).then(...) 但是,您不能很好地输入 JSON 文件,并且在语法和写出每个测试所需的所有属性时存在更多错误空间。 JSON 文件也离测试代码更远,因此您在编辑测试时必须管理至少两个文件。 如果所有环境测试数据都直接在cypress.json中的环境变量中设置,则会出现类似的维护问题,并且在过多的测试中管理太多。

另一种选择是在规范文件中创建一个测试夹具对象,该对象具有基于测试或暂存的属性,以加载该测试的用户和特定环境的元数据。 由于这些是对象,您还可以围绕测试夹具对象定义更好的通用 TypeScript 类型,以便所有规范文件重用并定义元数据类型。 您将调用Cypress.env(“testEnv”)来查看您正在运行的测试环境,并使用该值从整个测试夹具对象中提取相应环境的测试夹具,并在您的测试中使用这些值。 下面的代码片段总结了测试夹具对象的一般概念。

将“baseUrl”赛普拉斯配置值、“apiHost”后端环境变量和“testEnv”环境变量一起应用,我们可以让赛普拉斯测试在多个环境中工作,而无需添加多个条件或单独的逻辑流程,如下所示。

让我们退后一步,看看如何让自己的 Cypress 命令通过 npm 运行。 类似的概念可以应用于您可能用于您的应用程序的 yarn、Makefile 和其他脚本。 您可能希望定义“打开”和“运行”命令的变体,以与赛普拉斯“打开”GUI 并在无头模式下针对package.json中的各种前端和后端环境“运行”保持一致。 您还可以为每个环境的配置设置多个 JSON 文件,但为简单起见,您将看到带有内联选项和值的命令。

您会在package.json脚本中注意到,您的前端“baseUrl”范围从本地启动应用程序时的“http://localhost:9001”到部署的应用程序 URL,例如“ https://staging.app”。 com ”。 您可以设置后端“apiHost”和“testEnv”变量来帮助向后端端点发出请求并加载特定的测试夹具对象。 当您需要使用录制密钥在 Docker 容器中运行测试时,您还可以创建特殊的“cicd”命令。

一些外卖

当涉及到选择元素、与元素交互以及断言页面上的元素时,您可以使用一小部分赛普拉斯命令(例如cy.get()cy.contains()编写许多赛普拉斯测试。 .click() , .type() , .should('be.visible')

还有一些方法可以使用 cy.request() 向后端 API 发出 HTTP 请求,使用cy.request()在节点服务器中运行任意代码,以及使用cy.task() cy.server()cy.route()存根网络请求. 您甚至可以创建自己的自定义命令,例如cy.login()来帮助您通过 API 登录用户。 所有这些都有助于在测试运行之前将用户重置到正确的起点。 将这些选择器和函数完全包装在一个文件中,您已经创建了可重用的页面对象以在您的规范中使用。

为了帮助您编写在多个环境中通过的测试,请利用环境变量和包含环境特定元数据的对象。

这将帮助您在赛普拉斯规范中使用不同的数据资源运行不同的用户集。 单独的赛普拉斯 npm 命令(如package.json中的npm run cypress:open:staging )将加载正确的环境变量值并针对您选择运行的环境运行测试。

这总结了我们对编写 Cypress 测试的一千英尺概述。 我们希望这为您提供了在您自己的赛普拉斯测试中应用和改进的实际示例和模式。

有兴趣了解有关 Cypress 测试的更多信息吗? 查看以下资源:

  • 编写 E2E 测试时要考虑什么
  • TypeScript 包含 Cypress 测试中的所有内容
  • 在 Cypress 测试中处理电子邮件流
  • 配置、组织和整合赛普拉斯测试的想法
  • 将 Cypress 测试与 Docker、Buildkite 和 CICD 集成