E2E 测试之旅第 2 部分:从 WebdriverIO 到 Cypress

已发表: 2019-11-21

注意:这是来自#frontend@twiliosendgrid 的帖子。 对于其他工程帖子,请转到技术博客卷。

在我们所有的前端应用程序中,我们拥有并且仍然拥有以下目标:提供一种方法来为我们的前端应用程序编写一致、可调试、可维护和有价值的 E2E(端到端)自动化测试,并与 CICD(持续集成和持续部署)。

为了达到我们今天在 SendGrid 的所有前端团队中可能触发或按计划运行数百个 E2E 测试的状态,我们必须在此过程中研究和试验许多潜在的解决方案,直到我们完成主要任务目标。

我们尝试推出由专门的测试工程师开发的自定义 Ruby Selenium 解决方案,称为 SiteTestUI aka STUI,在该解决方案中,所有团队都可以贡献一个 repo 并运行跨浏览器自动化测试。 可悲的是,它屈服于缓慢、不稳定的测试、误报、repos 和语言之间缺乏上下文、一个篮子里有太多的手、痛苦的调试经历,以及花费在维护上的时间多于提供价值的时间。

然后,我们在 WebdriverIO 中尝试了另一个有前途的库,用 JavaScript 编写测试,这些测试与每个团队的应用程序存储库位于同一位置。 虽然这解决了一些问题,赋予了更多的团队所有权,并允许我们将测试与我们的 CICD 提供商 Buildkite 集成,但除了处理所有太相似的 Selenium 错误和怪癖之外,我们仍然有一些难以调试和难以编写的不稳定测试.

我们想避免另一个 STUI 2.0 并开始探索其他选项。 如果您想了解更多关于我们从 STUI 迁移到 WebdriverIO 过程中的经验教训和发现的策略,请查看博客文章系列的第 1 部分。

在博文系列的最后第二部分,我们将介绍从 STUI 和 WebdriverIO 到 Cypress 的旅程,以及我们如何在设置整体基础架构、编写有组织的 E2E 测试、与我们的 Buildkite 管道集成以及扩展到组织中的其他前端团队。

TLDR:我们在 STUI 和 WebdriverIO 上采用了 Cypress,并且我们完成了编写有价值的 E2E 测试以与 CICD 集成的所有目标。 我们从 WebdriverIO 和 STUI 中学到的大量工作和经验很好地延续到了我们今天如何使用和集成赛普拉斯测试。

目录

探索并登陆 Cypress

从 STUI 和 WebdriverIO 切换到 Cypress

第 1 步:为 Cypress 安装依赖项

第 2 步:环境配置和脚本

环境配置和脚本的第一遍

环境配置和脚本的演变

第 3 步:在本地实施 E2E 测试

第 4 步:对测试进行 Docker 化

第 5 步:与 CICD 集成

第 6 步:比较 Cypress 与 WebdriverIO/STUI

第 7 步:扩展到其他前端团队

我们对赛普拉斯的期待

采用赛普拉斯走向未来

探索并登陆 Cypress

当我们搜索 WebdriverIO 的替代品时,我们看到了其他 Selenium 包装器,例如 Protractor 和 Nightwatch,它们具有与 WebdriverIO 类似的功能集,但我们认为我们很可能会遇到冗长的设置、不稳定的测试以及繁琐的调试和维护。

幸运的是,我们偶然发现了一个名为 Cypress 的新 E2E 测试框架,它展示了快速设置、在浏览器中运行的快速且可调试的测试、网络层请求存根,最重要的是,它没有在后台使用 Selenium。

我们惊叹于视频录制、赛普拉斯 GUI、付费仪表板服务和并行化等令人惊叹的功能。 我们愿意在跨浏览器支持上做出妥协,以支持有价值的测试持续通过 Chrome,我们可以使用一系列工具来为我们的开发人员和 QA 实施、调试和维护测试。

我们还赞赏为我们选择的测试框架、断言库和所有其他工具,以便在我们所有的前端团队中拥有更标准化的测试方法。 下面,我们提供了 WebdriverIO 和 Cypress 等解决方案之间差异的屏幕截图,如果您想了解 Cypress 和 Selenium 解决方案之间的更多差异,您可以查看他们的文档以了解其工作原理。

考虑到这一点,我们承诺测试 Cypress 作为另一种解决方案,用于编写快速、一致和可调试的 E2E 测试,最终在 CICD 期间与 Buildkite 集成。 我们还特意设定了另一个目标,将 Cypress 与之前基于 Selenium 的解决方案进行比较,最终通过数据点确定我们是否应该继续寻找或使用 Cypress 构建我们的测试套件。 我们计划将现有的 WebdriverIO 测试和仍在 STUI 中的任何其他高优先级测试转换为赛普拉斯测试,并比较我们的开发人员体验、速度、稳定性、测试运行时间和测试维护。

从 STUI 和 WebdriverIO 切换到 Cypress

当从 STUI 和 WebdriverIO 切换到 Cypress 时,我们通过我们在前端应用程序存储库中尝试从 STUI 迁移到 WebdriverIO 时使用的相同高级策略系统地解决了这个问题。 有关我们如何为 WebdriverIO 完成这些步骤的更多详细信息,请参阅博客文章系列的第 1 部分。 过渡到赛普拉斯的一般步骤包括以下内容:

  1. 安装和设置依赖项以连接 Cypress
  2. 建立环境配置和脚本命令
  3. 实施针对不同环境在本地通过的端到端测试
  4. Docker 化测试
  5. 将 Dockerized 测试与我们的 CICD 提供商 Buildkite 集成

为了实现我们的次要目标,我们还添加了额外的步骤来比较 Cypress 与之前的 Selenium 解决方案,并最终在组织中的所有前端团队中扩展 Cypress:

6. 在开发人员体验、速度和测试稳定性方面比较 Cypress 与 WebdriverIO 和 STUI
7. 扩展到其他前端团队

第 1 步:为 Cypress 安装依赖项

为了快速启动和运行赛普拉斯,我们所要做的就是在我们的项目中使用“npm install cypress”并首次启动赛普拉斯,以便使用“cypress.json”配置文件和cypress文件夹自动布局带有用于命令和插件的启动装置、测试和其他设置文件。 我们很欣赏 Cypress 如何与 Mocha 捆绑在一起作为测试运行程序,Chai 用于断言,Chai-jQuery 和 Sinon-Chai 用于更多的断言以供使用和链接。 与刚开始使用 WebdriverIO 或 STUI 时相比,我们不再需要花费大量时间来研究要安装和使用哪些测试运行程序、报告程序、断言和服务库。 我们立即使用他们的 Cypress GUI 运行了一些生成的测试,并探索了我们可以使用的许多调试功能,例如时间旅行调试、选择器游乐场、录制的视频、屏幕截图、命令日志、浏览器开发工具等。

我们稍后还使用 Eslint 和 TypeScript 对其进行了设置,以便在提交新的赛普拉斯测试代码时遵循额外的静态类型检查和格式化规则。 我们最初在 TypeScript 支持方面遇到了一些问题,有些文件需要是 JavaScript 文件,比如那些以插件文件为中心的文件,但在大多数情况下,我们能够对我们的大部分文件进行类型检查,以获取我们的测试、页面对象和命令。

这是我们的一个前端团队遵循的示例文件夹结构,用于合并页面对象、插件和命令:

第 2 步:环境配置和脚本

在快速安装和设置赛普拉斯以在本地运行后,我们需要一种方法让赛普拉斯测试在每个环境的不同设置下运行,并希望支持我们的 WebdriverIO 命令允许我们执行的相同用例。 这是一个列表,说明了我们希望如何执行这些测试以及为什么我们希望支持它们的大多数用例:

  • 针对在 localhost(即 http://localhost:8000)上运行的 Webpack 开发服务器,该开发服务器将指向某个环境 API(即 https://testing.api.com 或 https://staging.api。 com) 喜欢测试或分期。
    为什么? 有时我们需要对本地 Web 应用程序进行更改,例如为我们的测试添加更多特定的选择器,以便以更健壮的方式与元素交互,或者我们正在开发新功能并需要调整和验证现有的自动化测试将针对我们的新代码更改在本地传递。 每当应用程序代码更改并且我们还没有推送到已部署的环境时,我们使用此命令针对我们的本地 Web 应用程序运行我们的测试。
  • 针对特定环境(即 https://testing.app.com 或 https://staging.app.com)的已部署应用程序,例如测试或登台
    为什么? 其他时候应用程序代码不会改变,但我们可能不得不改变我们的测试代码来修复一些脆弱性,或者我们有足够的信心在不进行任何前端更改的情况下完全添加或删除测试。 我们大量使用此命令来针对已部署的应用程序在本地更新或调试测试,以更接近地模拟我们的测试在 CICD 中的运行方式。
  • 在 Docker 容器中针对特定环境(如测试或登台)部署的应用程序运行
    为什么? 这适用于 CICD,因此我们可以触发 E2E 测试在 Docker 容器中运行,例如针对暂存部署的应用程序,并确保它们在将代码部署到生产之前或在专用管道中的预定测试运行中通过。 最初设置这些命令时,我们进行了大量试验和错误,以使用不同的环境变量值启动 Docker 容器,并在将其与我们的 CICD 提供程序 Buildkite 挂钩之前,测试是否成功执行了正确的测试。

环境配置和脚本的第一遍

当我们第一次尝试设置 Cypress 时,我们在包含 https://app.sendgrid.com 的存储库中进行了设置,这是一个包含发件人身份验证、电子邮件活动和电子邮件验证等功能页面的 Web 应用程序,我们不可避免地分享了我们与营销活动网络应用程序背后的团队的发现和学习,其中包含 https://mc.sendgrid.com 域。 我们希望针对我们的暂存环境运行 E2E 测试,并利用赛普拉斯的命令行界面和--config--env等选项来完成我们的用例。

为了在http://127.0.0.1:8000或部署的暂存应用程序 URL 上对本地 Web 应用程序运行赛普拉斯测试,我们调整了命令中的baseUrl配置标志并添加了额外的环境变量,例如testEnv以提供帮助在我们的赛普拉斯测试中加载某些固定装置或特定环境的测试数据。 例如,使用的 API 密钥、创建的用户和其他资源可能因环境而异。 如果环境中不支持某些功能或测试设置不同,我们会使用testEnv来切换这些固定装置或添加特殊的条件逻辑,并且我们将通过我们规范中的Cypress.env(“testEnv”)类的调用来访问环境。

然后,我们组织了命令cypress:open:*来表示打开 Cypress GUI,以便我们在本地开发时选择要通过 UI 运行的测试,并组织cypress:run:*来表示在无头模式下执行测试,这更加定制用于在 CICD 期间在 Docker 容器中运行。 在openrun之后出现的是环境,因此我们的命令可以像npm run cypress:open:localhost:staging一样轻松读取,以打开 GUI 并针对指向 staging API 或npm run cypress:run:staging的本地 Webpack 开发服务器运行测试npm run cypress:run:staging以无头模式针对已部署的登台应用程序和 API 运行测试。 package.json赛普拉斯脚本是这样出来的:

环境配置和脚本的演变

在另一个项目中,我们改进了赛普拉斯命令和配置,以利用cypress/plugins/index.js文件中的一些节点逻辑来拥有一个基本的cypress.json文件和单独的配置文件,这些文件将基于名为的环境变量读取configFile加载特定的配置文件。 然后将加载的配置文件与基本文件合并,最终指向登台或模拟后端服务器。

如果您想了解更多关于模拟后端服务器的信息,我们开发了一个 Express 服务器,其后端端点仅根据请求中传递的查询参数返回静态 JSON 数据和状态代码(即 200、4XX、5XX)的不同响应。 这使前端畅通无阻,可以继续通过对模拟后端服务器的实际网络调用来开发页面流,并通过响应模拟实际 API 在未来可用时的样子。 我们还可以针对我们不同的 UI 状态轻松模拟不同级别的成功和错误响应,否则这些状态将难以在生产中重现,并且由于我们将进行确定性网络调用,因此我们的 Cypress 测试在启动相同网络时不会那么不稳定每次请求和响应。

我们有一个基本的cypress.json文件,其中包括用于一般超时的共享属性、用于连接 Cypress Dashboard Service 的项目 ID(我们将在稍后讨论)以及其他设置,如下所示:

我们在cypress文件夹中创建了一个config文件夹来保存我们的每个配置文件,例如localhostMock.json以针对本地模拟 API 服务器运行我们的本地 Webpack 开发服务器或staging.json以针对已部署的登台应用程序和 API 运行。 这些要与基本配置进行比较和合并的配置文件如下所示:

CICD 配置文件有一个更简单的 JSON 文件,因为我们需要动态设置环境变量以考虑我们稍后将深入研究的不同 Docker 服务前端基本 URL 和模拟服务器 API 主机。

在我们的cypress/plugins/index.js文件中,我们添加了从 Cypress 命令读取名为configFile的环境变量的逻辑,该命令最终将读取config文件夹中的相应文件并将其与基本cypress.json合并,如下所示:

为了使用为我们的用例设置的环境变量编写合理的赛普拉斯命令,我们利用了类似于以下内容的Makefile

将这些命令整齐地排列在 Makefile 中,我们可以在我们的 `package.json` npm 脚本中快速执行诸如make cypress_open_stagingmake cypress_run_staging类的操作。

以前,我们习惯将一些命令放在一行很长的行中,这样很难在没有错误的情况下进行编辑。 值得庆幸的是,Makefile 通过将环境变量的可读内插到多行的赛普拉斯命令中,帮助更好地分散了内容。 我们可以快速设置或导出环境变量,例如configFile用于加载哪个环境配置文件, BASE_URL用于访问我们的页面, API_HOST用于不同的后端环境,或者SPECS以确定在我们启动任何 Makefile 命令之前运行哪些测试。

我们还将 Makefile 命令用于其他冗长的 npm 脚本和 Docker 命令,例如构建我们的 Webpack 资产、安装依赖项或与其他命令同时运行。 最后,我们会将一些 Makefile 命令翻译到package.json脚本部分,但如果有人只想使用 Makefile,这不是必需的,它看起来如下所示:

我们故意省略了很多 Cypress CICD 命令,因为它们不是日常开发中使用的命令,因此使package.json更加精简。 最重要的是,我们可以一目了然地看到所有与模拟服务器和本地 Webpack 开发服务器相关的赛普拉斯命令与暂存环境,以及哪些是“打开”GUI 而不是在无头模式下“运行”。

第 3 步:在本地实施 E2E 测试

当我们开始使用 Cypress 实施 E2E 测试时,我们引用了来自 WebdriverIO 和 STUI 的现有测试来转换并为其他高优先级功能添加了更新的测试,从简单的健康检查到复杂的快乐路径流。 将现有的页面对象和测试文件从 WebdriverIO 或 STUI 转换为 Cypress 中的等效页面对象和规范被证明是轻而易举的事。 它实际上产生了比以前更简洁的代码,更少的明确等待元素以及更好的断言和其他赛普拉斯命令的链接性。

例如,从最终用户的角度来看,测试的一般步骤保持不变,因此转换工作涉及通过以下方式将 WebdriverIO 或 STUI API 映射到 Cypress API:

  • 许多命令本质上出现并且工作起来类似于我们几乎只是用cyCypress替换$browser即通过$(“.button”).click()cy.get(“.button”).click()访问页面.click cy.get(“.button”).click()browser.url()cy.visit() ,或$(“.input”).setValue()cy.get(“.input”).type()
  • 使用$$$通常会变成cy.get(...)cy.contains(...)$$(“.multiple-elements-selector”)$(“.single-element-selector”)变成cy.get(“.any-element-selector”)cy.contains(“text”)cy.contains(“.any-selector”)
  • 删除无关$(“.selector”).waitForVisible(timeoutInMs)$(“.selector”).waitUntil(...)$(“.selector”).waitForExist()调用,以支持默认情况下让 Cypress使用cy.get('.selector')cy.contains(textInElement)处理重试和检索元素。 如果我们需要比默认更长的超时时间,我们将完全使用cy.get('.selector', { timeout: longTimeoutInMs }) ,然后在检索元素后,我们将链接下一个操作命令以对元素执行某些操作,即cy.get(“.selector”).click()
  • 使用浏览器的自定义命令。 addCommand('customCommand, () => {})` turned into `Cypress.Commands.add('customCommand', () => {})并执行 `cy.customCommand()`
  • 使用名为node-fetch的库通过 API 发出网络请求以进行设置或拆卸,并将其包装在browser.call(() => return fetch(...))和/或browser.waitUntil(...)中导致通过cy.request(endpoint)或我们定义的自定义插件在赛普拉斯节点服务器中发出 HTTP 请求,并发出类似cy.task(taskToHitAPIOrService)的调用。
  • 在我们不得不等待一个重要的网络请求可能在没有任何显着 UI 更改的情况下完成之前,我们有时不得不求助于使用browser.pause(timeoutInMs) ,但是使用 Cypress,我们通过网络存根功能改进了它并且能够监听并使用cy.server()cy.route(“method”, “/endpoint/we/are/waiting/for).as(“endpoint”)`, and `cy.wait(“@endpoint”)在启动将触发请求的操作之前。

在将大量 WebdriverIO 语法和命令转换为赛普拉斯命令后,我们引入了相同的概念,即为通用共享功能提供基本页面对象,并为测试所需的每个页面提供扩展页面对象。 这是一个基本页面对象的示例,该对象具有可在所有页面之间共享的通用open()功能。

扩展的页面对象将为元素选择器添加 getter,使用其页面路由实现open()功能,并提供任何帮助器功能,如下所示。

我们实际的扩展页面对象还使用了一个简单的对象映射来将我们所有的 CSS 元素选择器保存在一个地方,我们可以将其作为数据属性插入到 React 组件中,在单元测试中引用,并在 Cypress 页面对象中用作我们的选择器。 此外,我们的页面对象类有时会在利用类构造函数方面有所不同,如果说一个页面对象被重用于一堆外观和功能相似的页面,比如我们的 Suppressions 页面,我们会传递参数来更改路由或特定属性。

附带说明一下,团队不需要使用页面对象,但我们赞赏将页面功能和 DOM 元素选择器引用与标准类对象结构保持在一起的模式的一致性,以便在所有页面之间共享通用功能。 其他团队更喜欢使用很少的实用功能和不使用 ES6 类来创建许多不同的文件,但重要的是要提供一种有组织的、可预测的方式来封装所有内容并编写测试以提高开发人员的效率和可维护性。

我们坚持使用与旧 WebdriverIO 测试相同的通用测试策略,并尝试通过 API 尽可能多地设置测试。 我们特别希望避免通过 UI 建立我们的设置状态,以免为我们不打算测试的部分引入片状和浪费时间。 大多数测试都涉及此策略:

  • 通过 API 设置或删除 - 如果我们需要通过 UI 测试创建实体,我们将确保首先通过 API 删除实体。 无论之前的测试运行如何以成功或失败告终,都需要通过 API 正确设置或拆除测试,以确保测试以一致的方式运行并以正确的条件开始。
  • 通过 API 登录到专门的测试用户——我们为每个页面甚至每个自动化测试创建了专门的测试用户,这样我们的测试将被隔离,并且在并行运行时不会占用彼此的资源。 我们通过 API 发出与登录页面相同的请求,并在测试开始之前存储 cookie,以便我们可以直接访问经过身份验证的页面并开始实际的测试步骤。
  • 从最终用户的角度自动执行步骤——通过 API 登录用户后,我们直接访问页面并自动执行最终用户完成功能流程并验证用户看到并与正确的事物交互的步骤一路上。

为了将测试重置为其预期的原始状态,我们将使用全局cy.login命令通过 API 登录到专用测试用户,设置 cookie 以保持用户登录,进行必要的 API 调用以返回用户通过cy.request(“endpoint”)cy.task(“pluginAction”)调用到达所需的起始状态,并访问我们试图直接测试的经过身份验证的页面。 然后,我们将自动执行这些步骤来完成用户功能流程,如下面的测试布局所示。

还记得我们谈到的登录自定义命令cy.login()和注销cy.logout()吗? 我们以这种方式在 Cypress 中轻松实现了它们,因此我们所有的测试都将以相同的方式通过 API 登录到用户。

此外,我们希望自动化和验证某些涉及电子邮件的复杂流程,而这些流程在以前使用 WebdriverIO 或 STUI 无法很好地完成。 一些示例包括将电子邮件活动导出到 CSV、通过“发送给同事”流程进行发件人身份验证,或将电子邮件验证结果导出到 CSV。 赛普拉斯阻止一个人在一次测试中访问多个超级域,因此通过我们不拥有的 UI 导航到电子邮件客户端是不稳定的,不是一种选择。

相反,我们通过他们的cy.task(“pluginAction”)命令开发了赛普拉斯插件,以使用赛普拉斯节点服务器中的一些库连接到测试电子邮件 IMAP 客户端/收件箱,例如 SquirrelMail,以便在提示操作后检查收件箱中的匹配电子邮件在 UI 中,并将这些电子邮件中的重定向链接返回到我们的 Web 应用程序域,以验证某些下载页面是否出现并有效地完成整个客户流程。 我们实现了插件,这些插件会在给定特定主题行的情况下等待电子邮件到达 SquirrelMail 收件箱,删除电子邮件,发送电子邮件,触发电子邮件事件,轮询后端服务,并通过 API 进行更有用的设置和拆卸以供我们的测试使用。

为了更深入地了解我们使用赛普拉斯实际测试的内容,我们介绍了许多高价值案例,例如:

  • 对我们所有页面的健康检查,也就是浏览应用程序——我们希望确保加载了某些内容的页面有时会导致某些后端服务或前端托管关闭。 我们还建议首先进行这些测试,以建立使用选择器和辅助函数构建页面对象的心理肌肉记忆,并针对环境进行快速、有效的测试。
  • 页面上的 CRUD 操作——我们将始终通过 API 相应地重置测试,然后专门测试 UI 中的创建、读取、更新或删除。 例如,如果我们测试能够通过 UI 创建域身份验证,无论最后一次测试运行如何结束,我们都需要确保我们要通过 UI 创建的域首先通过 API 删除,然后再继续使用自动化 UI 步骤来创建域并避免冲突。 如果我们测试了能够通过 UI 删除抑制,我们确保首先通过 API 创建抑制,然后继续执行这些步骤。
  • 在页面上测试搜索过滤器——我们测试了使用电子邮件活动设置一堆高级搜索过滤器,并使用查询参数访问页面,以确保过滤器是自动填充的。 我们还通过电子邮件验证 API 添加了数据,并再次启动了不同的搜索过滤器并验证了该表与该页面中的搜索过滤器匹配。
  • 不同的用户访问权限——在 Twilio SendGrid,我们的父帐户可以拥有具有不同范围或访问权限的队友,或者其下的子用户也具有不同程度的访问权限,并且行为与父帐户有些相似。 对某些页面和子用户具有只读权限和管理员权限的队友会看到或看不到页面上的某些内容,这使得自动登录这些类型的用户并检查他们在赛普拉斯测试中看到或看不到的内容变得容易。
  • 不同的用户包——我们的用户也可以在免费到付费包的类型上有所不同,例如 Essentials、Pro 和 Premier,这些包也可以在页面上看到或看不到某些内容。 我们将使用不同的软件包登录用户,并快速验证用户在赛普拉斯测试中可以访问的功能、副本或页面。

第 4 步:对测试进行 Docker 化

在云中的新 AWS 机器上执行每个 Buildkite 管道步骤时,我们不能简单地调用npm run cypress:run:staging ,因为这些机器没有 Node、浏览器、我们的应用程序代码或任何其他依赖项来实际运行 Cypress测试。 当我们之前设置 WebdriverIO 时,我们需要在 Docker Compose 文件中组装三个独立的服务,以使适当的 Selenium、Chrome 和应用程序代码服务一起运行以运行测试。

使用赛普拉斯,这要简单得多,因为我们只需要赛普拉斯基础 Docker 映像cypress/base来在 Dockerfile 中设置环境,并且在Dockerfile docker-compose.yml文件中只需要一个服务以及我们的应用程序代码来运行赛普拉斯测试。 我们将介绍一种方法,因为还有其他赛普拉斯 Docker 映像可供使用,以及在 Docker 中设置赛普拉斯测试的其他方法。 我们鼓励您查看赛普拉斯文档以获取替代方案

为了提供运行赛普拉斯测试所需的所有应用程序和测试代码的服务,我们制作了一个名为DockerfileDockerfile.cypress并安装了所有node_modules并将代码复制到 Node 环境中映像的工作目录中。 这将被我们的cypress Docker Compose 服务使用,我们通过以下方式实现了Dockerfile设置:

有了这个Dockerfile.cypress ,我们可以集成 Cypress 以针对特定环境 API 运行选定的规范,并通过一个名为cypress的 Docker Compose 服务部署应用程序。 我们所要做的就是插入一些环境变量,例如SPECSBASE_URL ,以通过npm run cypress:run:cicd:staging命令对某个基本 URL 运行选定的赛普拉斯测试,该命令如下所示, ”cypress:run:cicd:staging”: “cypress run --record --key --config baseUrl=$BASE_URL --env testEnv=staging”

这些环境变量将通过 Buildkite 管道的设置/配置文件进行设置,或者在触发赛普拉斯测试以从我们的部署管道运行时动态导出。 一个示例docker-compose.cypress.yml文件看起来类似于:

还有一些其他的事情需要观察。 例如,您可以看到VERSION环境变量,它允许我们引用特定标记的 Docker 映像。 稍后我们将演示如何标记 Docker 映像,然后为该构建拉取相同的 Docker 映像,以针对赛普拉斯测试的正确代码运行。

此外,您还会注意到传递的BUILDKITE_BUILD_ID ,它与我们启动的每个构建的其他 Buildkite 环境变量以及ci-build-id标志一起免费提供。 这启用了赛普拉斯的并行化功能,当我们为赛普拉斯测试分配一定数量的机器时,它会自动神奇地知道如何启动这些机器并将我们的测试分开以在所有这些机器节点上运行,以优化和加速我们的测试运行时间。

我们最终还利用了卷安装和 Buildkite 的工件功能。 我们上传视频和屏幕截图,以便通过 Buildkite UI 的“Artifacts”选项卡直接访问,以防我们用完本月分配的付费测试记录或无法访问 Dashboard 服务。 每当在无头模式下运行赛普拉斯“运行”命令时, cypress/videoscypress/screenshots文件夹中都会有输出供人们在本地查看,我们只需挂载这些文件夹并将它们上传到 Buildkite 以防万一。

第 5 步:与 CICD 集成

一旦我们让赛普拉斯测试在 Docker 容器中针对不同环境成功运行,我们就开始与我们的 CICD 提供商 Buildkite 集成。 Buildkite 提供了在我们的 AWS 机器上执行.yml文件中的步骤的方法,其中 Bash 脚本和环境变量在代码中或通过 Web UI 中的 repo 的 Buildkite 管道设置设置。 Buildkite 还允许我们使用导出的环境变量从我们的主部署管道触发这个测试管道,我们可以将这些测试步骤重用于其他隔离的测试管道,这些测试管道将按计划运行,供我们的 QA 监控和查看。

在高层次上,我们为 Cypress 测试的 Buildkite 管道以及我们之前的 WebdriverIO 管道共享以下类似步骤:

  • 设置 Docker 映像。 构建、标记测试所需的 Docker 映像并将其推送到注册表,以便我们可以在后面的步骤中将其拉下。
  • 根据环境变量配置运行测试。 为特定构建拉下标记的 Docker 映像,并针对已部署的环境执行正确的命令,以从设置的环境变量中运行选定的测试套件。

这是一个pipeline.cypress.yml文件的示例,该文件演示了在“构建赛普拉斯 Docker 映像”步骤中设置 Docker 映像并在“运行赛普拉斯测试”步骤中运行测试:

需要注意的一点是第一步,“构建 Cypress Docker 映像”,以及它如何为测试设置 Docker 映像。 它使用 Docker Compose build命令使用所有应用程序测试代码构建cypress服务,并使用latest${VERSION}环境变量对其进行标记,因此我们最终可以在未来的一步。 每个步骤都可能在 AWS 云中某处的不同机器上执行,因此标签唯一地标识特定 Buildkite 运行的映像。 标记图像后,我们将最新和版本标记的图像推送到我们的私有 Docker 注册表以供重用。

在“运行赛普拉斯测试”步骤中,我们拉下我们在第一步中构建、标记和推送的映像,并启动赛普拉斯服务以执行测试。 基于SPECSBASE_URL等环境变量,我们将针对特定的已部署应用程序环境运行特定的测试文件,以实现特定的 Buildkite 构建。 这些环境变量将通过 Buildkite 管道设置进行设置,或者从 Bash 脚本动态触发,该脚本将解析 Buildkite 选择字段以确定要运行哪些测试套件以及针对哪个环境运行。

当我们选择在 Buildkite CICD 部署管道期间运行哪些测试并使用某些导出的环境变量触发专用触发测试管道时,我们按照pipeline.cypress.yml文件中的步骤来实现它。 将一些新代码从部署管道部署到功能分支环境后触发测试的示例如下所示:

触发测试将在单独的管道中运行,并且在点击“Build #639”链接后,它将带我们进入触发测试运行的构建步骤,如下所示:

为按计划运行的专用赛普拉斯 Buildkite 管道重复使用相同的pipeline.cypress.yml文件,我们有构建,例如运行我们的“P1”,最高优先级 E2E 测试的构建,如下图所示:

我们所要做的就是为诸如要运行的规范以及要在 Buildkite 管道设置中命中的后端环境等设置适当的环境变量。 然后,我们可以配置一个 Cron 计划构建,它也在管道设置中,每隔一定小时启动一次,我们就可以开始了。 We would then create many other separate pipelines for specific feature pages as needed to run on a schedule in a similar way and we would only vary the Cron schedule and environment variables while once again uploading the same `pipeline.cypress.yml` file to execute.

In each of those “Run Cypress tests” steps, we can see the console output with a link to the recorded test run in the paid Dashboard Service, the central place to manage your team's test recordings, billing, and other Cypress stats. Following the Dashboard Service link would take us to a results view for developers and QAs to take a look at the console output, screenshots, video recordings, and other metadata if required such as this:

Step 6: Comparing Cypress vs. WebdriverIO/STUI

After diving into our own custom Ruby Selenium solution in STUI, WebdriverIO, and finally Cypress tests, we recorded our tradeoffs between Cypress and Selenium wrapper solutions.

优点

  • It's not another Selenium wrapper – Our previous solutions came with a lot of Selenium quirks, bugs, and crashes to work around and resolve, whereas Cypress arrived without the same baggage and troubles to deal with in allowing us full access to the browser.
  • More resilient selectors – We no longer had to explicitly wait for everything like in WebdriverIO with all the $(.selector).waitForVisible() calls and now rely on cy.get(...) and c y.contains(...) commands with their default timeout. It will automatically keep on retrying to retrieve the DOM elements and if the test demanded a longer timeout, it is also configurable per command. With less worrying about the waiting logic, our tests became way more readable and easier to chain.
  • Vastly improved developer experience – Cypress provides a large toolkit with better and more extensive documentation for assertions, commands, and setup. We loved the options of using the Cypress GUI, running in headless mode, executing in the command-line, and chaining more intuitive Cypress commands.
  • Significantly better developer efficiency and debugging – When running the Cypress GUI, one has access to all of the browser console to see some helpful output, time travel debug and pause at certain commands in the command log to see before and after screenshots, inspect the DOM with the selector playground, and discern right away at which command the test failed. In WebdriverIO or STUI we struggled with observing the tests run over and over in a browser and then the console errors would not point us toward and would sometimes even lead us astray from where the test really failed in the code. When we opted to run the Cypress tests in headless mode, we got console errors, screenshots, and video recordings. With WebdriverIO we only had some screenshots and confusing console errors. These benefits resulted in us cranking out E2E tests much faster and with less overall time spent wondering why things went wrong. We recorded it took less developers and often around 2 to 3 times less days to write the same level of complicated tests with Cypress than with WebdriverIO or STUI.
  • Network stubbing and mocking – With WebdriverIO or STUI, there was no such thing as network stubbing or mocking in comparison to Cypress. Now we can have endpoints return certain values or we can wait for certain endpoints to finish through cy.server() and cy.route() .
  • Less time to set up locally – With WebdriverIO or STUI, there was a lot of time spent up front researching which reporters, test runners, assertions, and services to use, but with Cypress, it came bundled with everything and started working after just doing an npm install cypress.
  • Less time to set up with Docker – There are a bunch of ways to set up WebdriverIO with Selenium, browser, and application images that took us considerably more time and frustration to figure out in comparison to Cypress's Docker images to use right out of the gate.
  • Parallelization with various CICD providers – We were able to configure our Buildkite pipelines to spin up a certain number of AWS machines to run our Cypress tests in parallel to dramatically speed up the overall test run time and uncover any flakiness in tests using the same resources. The Dashboard Service would also recommend to us the optimal number of machines to spin up in parallel for the best test run times.
  • Paid Dashboard Service – When we run our Cypress tests in a Docker container in a Buildkite pipeline during CICD, our tests are recorded and stored for us to look at within the past month through a paid Dashboard Service. We have a parent organization for billing and separate projects for each frontend application to check out console output, screenshots, and recordings of all of our test runs.
  • Tests are way more consistent and maintainable – Tests passed way more consistently with Cypress in comparison to WebdriverIO and STUI where the tests kept on failing so much to the point where they were often ignored. Cypress tests failing more often signaled actual issues and bugs to look into or suggested better ways to refactor our tests to be less flaky. With WebdriverIO and STUI, we wasted a lot more time in maintaining those tests to be somewhat useful, whereas with Cypress, we would every now and then adjust the tests in response to changes in the backend services or minor changes in the UI.
  • Tests are faster – Builds passed way more consistently and overall test run times would be around 2 to 3 times faster when run serially without parallelization. We used to have overall test runs that would take hours with STUI and around 40 minutes with WebdriverIO, but now with way more tests and with the help of parallelization across many machine nodes, we can run over 200 tests in under 5 minutes .
  • Room to grow with added features in the future – With a steady open-source presence and dedicated Cypress team working towards releasing way more features and improvements to the Cypress infrastructure, we viewed Cypress as a safer bet to invest in rather than STUI, which would require us to engineer and solve a lot of the headaches ourselves, and WebdriverIO, which appeared to feel more stagnant in new features added but with the same baggage as other Selenium wrappers.

缺点

  • Lack of cross-browser support – As of this writing, we can only run our tests against Chrome. With WebdriverIO, we could run tests against Chrome, Firefox, Safari, and Opera. STUI also provided some cross-browser testing, though in a much limited form since we created a custom in-house solution
  • Cannot integrate with some third-party services – With WebdriverIO, we had the option to integrate with services like BrowserStack and Sauce Labs for cross-browser and device testing. However, with Cypress there are no such third-party integrations but there are some plugins with services like Applitools for visual regression testing available. STUI, on the other hand, also had some small integrations with TestRail , but as a compromise, we log out the TestRail links in our Cypress tests so we can refer back to them if we needed to.
  • Requires workarounds to test with iframes – There are some issues around handling iframes with Cypress. We ended up creating a global Cypress command to wrap how to deal with retrieving an iframe's contents as there is no specific API to deal with iframes like how WebdriverIO does.

To summarize our STUI, WebdriverIO, and Cypress comparison, we analyzed the overall developer experience (related to tools, writing tests, debugging, API, documentation, etc.), test run times, test passing rates, and maintenance as displayed in this table:

Following our analysis of the pros and cons of Cypress versus our previous solutions, it was pretty clear Cypress would be our best bet to accomplish our goal of writing fast, valuable, maintainable, and debuggable E2E tests we could integrate with CICD.

Though Cypress lacked features such as cross-browser testing and other integrations with third-party services that we could have had with STUI or WebdriverIO, we most importantly need tests that work more often than not and with the right tools to confidently fix broken ones. If we ever needed cross-browser testing or other integrations we could always still circle back and use our knowledge from our trials and experiences with WebdriverIO and STUI to still run a subset of tests with those frameworks.

We finally presented our findings to the rest of the frontend organization, engineering management, architects, and product. Upon demoing the Cypress test tools and showcasing our results between WebdriverIO/STUI and Cypress, we eventually received approval to standardize and adopt Cypress as our E2E testing library of choice for our frontend teams.

Step 7: Scaling to Other Frontend Teams

After successfully proving that using Cypress was the way to go for our use cases, we then focused on scaling it across all of our frontend teams' repos. We shared lessons learned and patterns of how to get up and running, how to write consistent, maintainable Cypress tests, and of how to hook those tests up during CICD or in scheduled Cypress Buildkite pipelines.

To promote greater visibility of test runs and gain access to a private monthly history of recordings, we established our own organization to be under one billing method to pay for the Dashboard Service with a certain recorded test run limit and maximum number of users in the organization to suit our needs.

Once we set up an umbrella organization, we invited developers and QAs from different frontend teams and each team would install Cypress, open up the Cypress GUI, and inspect the “Runs” and “Settings” tab to get the “Project ID” to place in their `cypress.json` configuration and “Record Key” to provide in their command options to start recording tests to the Dashboard Service. Finally, upon successfully setting up the project and recording tests to the Dashboard Service for the first time, logging into the Dashboard Service would show that team's repo under the “Projects” tab like this:

When we clicked a project like “mako”, we then had access to all of the test runs for that team's repo with quick access to console output, screenshots, and video recordings per test run upon clicking each row as shown below:

For more insights into our integration, we set up many separate dedicated test pipelines to run specific, crucial page tests on a schedule like say every couple hours to once per day. We also added functionality in our main Buildkite CICD deploy pipeline to select and trigger some tests against our feature branch environment and staging environment.

正如人们所预料的那样,这很快就通过了我们分配的本月记录的测试运行,特别是因为有多个团队以各种方式贡献和触发测试。 根据经验,我们建议注意计划运行的测试数量、运行这些测试的频率以及在 CICD 期间运行的测试。 可能有一些冗余的测试运行和其他更节俭的领域,例如回拨预定测试运行的频率,或者可能完全摆脱一些仅在 CICD 期间触发测试。

节俭的规则同样适用于添加用户,因为我们强调只向前端团队中的开发人员和质量保证人员提供访问权限,他们将大量使用仪表板服务,而不是高层管理人员和这些团队之外的其他人来填补这些有限的位置。

我们对赛普拉斯的期待

正如我们之前提到的,赛普拉斯在开源社区中展示了巨大的增长潜力和潜力,并且其专门的团队负责为我们提供更多有用的功能,以供我们在 E2E 测试中使用。 我们强调的许多缺点目前正在得到解决,我们期待以下事情:

  • 跨浏览器支持——这是一个很大的支持,因为我们采用 Cypress 的很多阻力来自于它只使用 Chrome,而基于 Selenium 的解决方案支持 Firefox、Chrome 和 Safari 等浏览器。 值得庆幸的是,更可靠、可维护和可调试的测试为我们的组织赢得了胜利,我们希望在未来赛普拉斯团队发布此类跨浏览器支持时,通过更多跨浏览器测试来提升我们的测试套件。
  • 网络层重写——这也是一个巨大的问题,因为我们倾向于在较新的 React 领域大量使用 Fetch API,而在较旧的 Backbone/Marionette 应用程序领域,我们仍然使用 jQuery AJAX 和正常的基于 XHR 的调用。 我们可以轻松地在 XHR 区域中截取或监听请求,但必须使用 fetch polyfill 做一些变通的变通方法才能达到相同的效果。 网络层重写应该有助于减轻这些痛苦。
  • 仪表板服务的增量改进——我们最近已经看到仪表板服务的一些新的 UI 更改,我们希望继续看到它随着更多的统计可视化和有用数据的分解而增长。 我们还大量使用并行化功能,并经常在仪表板服务中查看我们失败的测试记录,因此对布局和/或功能的任何迭代改进都会很高兴看到。

采用赛普拉斯走向未来

对于我们的组织,我们重视在 Chrome 浏览器下运行赛普拉斯测试时的开发人员效率、调试和稳定性。 我们实现了一致的、有价值的测试,并且在未来减少了维护,并使用了许多工具来开发新的测试和修复现有的测试。

总的来说,我们可以使用的文档、API 和工具远远超过了任何缺点。 在体验了 Cypress GUI 和付费仪表板服务之后,我们绝对不想回到 WebdriverIO 或我们定制的 Ruby Selenium 解决方案。

我们将我们的测试与 Buildkite 连接起来,并实现了我们的目标,即为我们的前端应用程序提供一种编写一致、可调试、可维护和有价值的 E2E 自动化测试以与 CICD 集成的方法。 我们向其他前端团队、工程高层和产品所有者证明了采用赛普拉斯并放弃 WebdriverIO 和 STUI 的好处。

后来在 Twilio SendGrid 的前端团队中进行了数百次测试,我们在暂存环境中发现了许多错误,并且能够比以往更加自信地快速修复我们这边的任何不稳定的测试。 开发人员和 QA 不再害怕编写 E2E 测试,但现在期待为我们发布的每个新功能或每个可以使用更多覆盖范围的旧功能编写它们。