让我们深入了解赛普拉斯的端到端测试
已发表: 2022-03-10这篇文章是由我们在 LambdaTest 的亲爱的朋友赞助的,他们正在为全球这么多人提供更流畅的跨浏览器测试体验。 谢谢!
今天很难想象没有自动化测试的软件开发。 多种不同的测试程序将确保高质量。 作为测试的基础,我们可以使用许多单元测试。 最重要的是,在金字塔的中间,可以说是集成测试。 端到端测试位于最顶端,涵盖最关键的用例。 这第三种测试将是本文的重点。
但是,端到端测试确实存在一些令人担忧的缺陷:
- 端到端测试很慢,因此在每个持续集成和持续部署 (CI/CD) 策略中都构成了重大障碍。 不仅如此,想象一下完成一项任务、一项功能或任何其他实现——等待测试执行会耗尽每个人的耐心。
- 由于调试工作,这种端到端测试难以维护、容易出错并且在各个方面都非常昂贵。 各种因素都可能导致这种情况。 你的测试应该感觉像一个助手,而不是一个障碍。
- 开发人员最大的噩梦是不稳定的测试,这是一种以相同方式执行但导致不同结果的测试。 它就像一个“Heisenbug”,只有在你不测量正在测试的应用程序时才会出现——也就是说,如果你不看它。
但不要担心:您不必屈服于这些陷阱。 让我们看看如何防止其中的许多。 但是,我不会只承诺月亮而不交付。 在本指南中,我们将一起编写一些测试,我已在 GitHub 存储库中为您公开了这些测试。 通过这种方式,我希望向您展示端到端测试可以很有趣! 让我们开始吧。
什么是端到端测试?
在谈论端到端(或 E2E)测试时,我喜欢将其称为“基于工作流的”。 这句话很好地总结了端到端测试:它模拟实际的用户工作流程,并且应该包括尽可能多的功能区域和应用程序中使用的技术堆栈的一部分。 最后,计算机会伪装成客户,并试图表现得像真正的用户。 这些测试最适合对应用程序的整个系统施加恒定压力,因此,当整个应用程序堆栈存在时,它们是确保质量的重要措施。
让我们回想一下我们想要通过所有这些实现的目标。 我们知道,前端测试是一组测试 Web 应用程序 UI 的实践,包括它的功能。 有道理——通过这些措施,我们可以确保我们的应用程序正常工作,并且未来的任何更改都不会破坏我们的代码。 为了有效地实现这一点,您可能想知道您需要测试什么以及需要测试多少。
这是一个有效的问题。 您可能会在一个比喻中找到一个可能的答案:测试自动化金字塔,首先由 Mike Cohn 引入,并由 Martin Fowler 进一步说明,它展示了如何使测试高效。 我们在金字塔的最低层发现快速且廉价的单元测试,而在顶部发现耗时且昂贵的 UI 测试(端到端测试)。
解释这一点及其优缺点对于它自己的文章来说就足够了。 我想专注于一个层面。 如果有效地优先考虑,端到端测试尤其可以显着提高质量。 这样做,我们可以不断地使我们的系统处于压力之下,并确保我们的应用程序的主要功能正常工作。
我的赛普拉斯之旅
当我开始学习如何编写端到端测试时,我在 Behat(一个面向场景的行为驱动开发 (BDD) 框架)之上使用了 Mink(一个 PHP 库)。 我开始使用 Selenium,它具有所有优点和缺点。 因为我的团队已经开始大量使用 Vue.js,所以我们改为使用基于 JavaScript 的测试框架,以确保完美的集成和兼容性。 我们当时的选择是 Nightwatch.js,所以我从头开始构建了我们的新测试套件。
在此期间,我们经常偶然发现兼容性问题。 你可以称之为依赖地狱——更不用说我们在 Selenium 和后来的 WebDriver 中看到的所有限制。
- 在我们的团队中,我们无法确定 CI 的 Chrome 版本。 因此,如果发布了 Chrome 的更新,Nightwatch.js 的速度不够快,无法兼容,导致我们的测试管道出现许多故障。
- 由于 Nightwatch.js 的等待可能性与我们的产品没有最佳匹配,导致不稳定测试的测试端原因的数量开始上升。
因此,我们开始考虑重新构建我们的测试套件。 在参观了一次非会议后,我发现了赛普拉斯。
Cypress 是一个不使用 Selenium 或 WebDriver 的一体化测试框架。 该工具使用 Node.js 在特殊控制下启动浏览器。 该框架中的测试在浏览器级别运行,而不仅仅是远程控制。 这提供了几个优点。
简而言之,我选择这个框架的原因如下:
- 出色的调试能力
Cypress 的测试运行器可以通过快照跳回到应用程序的任何状态。 因此,我们可以直接看到错误及其之前的所有步骤。 此外,还有对 Chrome 的开发者工具 (DevTools) 的完全访问权限,并且点击次数被完全记录。 - 在测试或 UI 或 API 响应中等待操作的更好方法
Cypress 带来了隐式等待,因此不需要进行适当的检查。 您还可以让测试等待动画和 API 响应。 - 测试是用 JavaScript 编写的
这减轻了编写测试的学习曲线。 Cypress 的测试运行程序是开源的,因此符合我们的产品策略。
然而,这篇文章是一个指南,所以让我们停下来了解这些一般信息并开始吧。
入门
安装并启动赛普拉斯
让我们从头开始。 在谈论赛普拉斯时,我通常首先通过mkdir
创建一个新目录,然后立即安装赛普拉斯。 最简单的安装方法如下图所示:
一点提示:如果不想使用 npm,可以通过 Yarn 安装 Cypress:
yarn add cypress --dev
另一种方法是直接下载,使用赛普拉斯提供的 ZIP 文件夹。 而已! 安装完成后,您就可以开始了。
有两种方法可以开始运行赛普拉斯测试。 第一种是在控制台中启动 Cypress,然后无头运行您的测试:
./node_modules/.bin/cypress run
第二种方法是使用 Cypress 的一项简洁功能,即其集成的测试运行程序。 测试运行器是用于运行测试的 UI。 要启动它,您可以使用类似的命令:
./node_modules/.bin/cypress open
此命令将打开测试运行器。 当你第一次打开 Cypress 时,你会看到这个界面:
赛普拉斯提供了一些预先编写的示例测试来展示其功能并为您提供一些起点——这就是提供测试的原因。 让我们暂时忽略这些,因为我们想尽快编写自己的。 但是,请记住这个“集成测试”区域,因为它会解释以后会发生的很多魔法。
赛普拉斯结构的第一印象
现在是时候在选择的集成开发环境 (IDE) 中打开我们新创建的项目了。 如果您导航到此文件夹,您将看到以下测试结构:
smashing-example └── cypress └── fixtures └── integration └── plugins └── support └── cypress.json
让我们看看这些文件夹:
-
fixtures
在这里您可以找到与其他实体无关的固定测试数据。 因此,这里没有存储 ID,可以根据本地状态更改。 -
integration
您将在此处找到实际测试。 -
plugins
在这里,您可以使用现有的赛普拉斯插件或您自己的插件来扩展赛普拉斯。 -
support
在这里,您可以扩展赛普拉斯本身。 您自己的命令和助手位于此处。 -
cypress.json
在此处修改配置,包括环境。
好吧,我想我们现在可以在 Cypress 上找到我们的方法了,无论是测试运行程序还是源代码。 但是我们如何开始呢? 我们要测试什么?
选择一个测试用例
典型的端到端测试可能会变得复杂,尤其是在有很多步骤的情况下。 手动执行会花费很多时间。 由于这种复杂性,E2E 测试难以实现自动化并且运行缓慢。 因此,我们需要仔细决定哪些案例要自动化。
在我看来, “基于工作流”这个词很关键:我们会根据典型的用户故事来选择测试用例。 但是,由于运行时间的原因,不建议涵盖每个可用的工作流程。 因此,我们需要一种方法来确定测试用例的优先级。
在我的团队中,我们的项目有几个标准。 测试用例应该:
- 涵盖一个特性的最通用和最常用的工作流程,例如 CRUD 操作(术语“快乐路径”很好地描述了这些工作流程);
- 使用风险分析,涵盖最易受攻击的 E2E 测试工作流程(即错误会造成最大损害的地方);
- 避免重复覆盖;
- 如果单元测试更合适,则不一定使用(使用 E2E 测试来测试您的软件对错误的响应,而不是错误本身)。
要记住的第二件最重要的事情是只测试您明确想要测试的工作流程。 使您的测试工作所需的所有其他步骤应在测试之外使用 API 操作来完成,以避免对其进行测试。 这样,您将确保最短的测试运行时间,并在测试用例失败时获得清晰的结果。 将此工作流程视为最终用户会:专注于使用功能而不是技术实现。
例子:
如果您想在网上商店测试结帐流程,请不要执行所有其他步骤,例如创建产品和类别,即使您需要它们来处理结帐。 例如,使用 API 或数据库转储来制作这些东西,并仅为结帐配置测试。
示例:在 Smashing Magazine 中查找我的文章
我想为这个网站写一个测试,Smashing Magazine。 我不能保证这个测试永远是最新的,但我们希望它会持续下去。 无论哪种方式,您都可以在 GitHub 存储库中找到此示例。
创建我们的第一个 Cypress 测试
在integration
文件夹中,我们将首先创建一个新文件。 我们称之为find-author.spec.js
。 后缀.spec
代表“规范”。 就测试而言,这是指您的应用程序必须满足的给定功能或应用程序的技术细节。
要将这个空的 JavaScript 文件变成测试的主目录,我们首先要为测试套件提供其结构。 我们将使用名为describe
的方法。 describe()
或context()
用于包含和组织测试。 换句话说,这个方法作为我们测试的框架。 因此,我们的测试文件将如下所示:
// find-author.spec.js describe('Find authors at smashing', () => { //... });
下一步是创建实际测试。 我们将使用it
的方法。 it()
或specify()
用于表示实际测试。 如您所见,我们可以在一个文件中捕获多个测试,从而提供一些出色的结构化选项。
// find-author.spec.js describe('Find authors at smashing', () => { it('Find the author Ramona Schwering', () => { cy.log('This is our brand-new test'); }); });
小提示:如果您熟悉 Mocha,您可能已经注意到一些相似之处。 Cypress 建立在 Mocha 之上,因此语法是相同的。
好吧,让我们继续。 如果我们在 Cypress 的测试运行器中运行我们的测试,我们会注意到 Cypress 将打开一个浏览器来运行测试。 该浏览器如下面的截图所示:
恭喜! 我们已经编写了第一个测试! 当然,它没有多大作用。 我们需要继续。 让我们用生活来填满我们的考验。
用生命填满考验
测试网站时首先要做的是什么? 对,我们需要打开网站。 我们可以使用 Cypress 命令来做到这一点。 命令是什么,你可能想知道?
使用命令
端到端测试中主要使用两种类型的指令。 第一种指令,命令,代表测试中的各个步骤。 在赛普拉斯的上下文中,命令是赛普拉斯与您的网站交互所做的一切。 这种交互可以是任何东西——点击、向下滚动网站,甚至是找到一个元素。 因此,命令将是我们用来填充测试的重要内容之一。
因此,我们的第一个命令将是导航到该网站的命令smashingmagazine.com
。 此命令称为visit
。
使用它,我们的测试将如下所示:
// find-author.spec.js describe('Find authors at smashing', () => { it('Find the author Ramona Schwering', () => { cy.visit('https://www.smashingmagazine.com/'); }); });
我经常使用一个命令——你也会。 它被称为get
:
cy.get('selector');
该命令根据其选择器返回一个元素——类似于 jQuery 的$(…)
。 因此,您将使用此命令来查找要与之交互的部分。 通常,您会使用它来启动一系列命令。 但是等等——命令链是什么意思?
正如本文开头所提到的,所有测试以及与之相关的所有其他内容都是用 JavaScript 编写的。 您可以将测试(即语句)中的命令放在一个链中(换句话说,链式)。 这意味着命令可以将命令的主题(或返回值)传递给以下命令,正如我们从许多测试框架中所知道的那样。
好的,我们将使用get
命令启动一系列命令。 要使用get
找到一个元素,我们需要先找到它的选择器。 找到一个唯一的选择器是必不可少的,否则赛普拉斯会返回所有匹配的元素; 所以,请记住这一点,如果它是无意的,请避免它。
与元素交互
Cypress 本身有一个功能可以帮助您找到要使用的元素的选择器。 此功能称为 Selector Playground,它可以帮助您发现组件的唯一选择器或查看选择器或文本字符串的所有匹配元素。 因此,此功能可以在此任务中为您提供很多帮助。 要启用它,只需单击测试 UI 标题中的十字准线图标,然后将鼠标悬停在所需元素上:
如上面的屏幕截图所示,工具提示将在悬停时显示选择器,或者在单击元素时出现的十字线图标下方的这个小栏中显示选择器。 在这个栏中,您还可以看到有多少元素会匹配给定的选择器——在我们的例子中确保它的唯一性。
有时,那些自动生成的选择器可能不是您想要使用的选择器(例如,如果它们很长或难以阅读或不满足您的其他条件)。 在我看来,下面生成的选择器很难理解并且太长:
在这种情况下,我会回退到浏览器的 DevTools 来找到我独特的选择器。 您可能熟悉这些工具; 就我而言,我经常为此选择 Chrome。 但是,其他受支持的浏览器可能会提供类似的功能。 这个过程感觉类似于 Selector Playground,除了我们在“元素”选项卡中使用 DevTools 的功能。
为确保选择器是唯一的,我建议您在 DevTools 的代码视图中搜索它。 如果您只找到一个结果,您可以确信它是独一无二的。
您知道有许多不同的选择器类型吗? 根据种类的不同,测试的外观甚至行为都可能完全不同。 有些品种比其他品种更适合端到端测试。 如果您想知道使用哪些选择器来保持测试稳定和干净,我可以为您指出我的一篇文章,其中涵盖了这个问题。 赛普拉斯的开发人员自己在他们的最佳实践中就该主题提供了一些指导。
我们的测试作为一系列命令
好的,回到我们的测试。 在其中,我们要显示我们的工作流程:
“作为用户,我将搜索作者的文章,并通过其中一篇文章中的参考区域导航到作者的网站。”
我们将重现用户使用命令执行的步骤。 我将在完成的测试下方粘贴注释,其中将解释步骤:
// find-author.spec.js it('Find the author Ramona Schwering', () => { // Open the website cy.visit('https://www.smashingmagazine.com'); // Enter author's name in search field cy.get('#js-search-input').type('Ramona Schwering'); // Navigate to author's article cy.get('h2 > a').first().click(); // Open the author's page cy.get('.author-post__author-title').click(); });
此示例处理我们要测试的工作流。 赛普拉斯将执行此测试。 那么,是时候说“恭喜”了吗? 我们终于写完了第一个测试吗?
好吧,请仔细看看。 赛普拉斯将执行它,但它只会执行测试告诉它的操作,即您编写的任何内容。 如果你在测试运行器中运行它,你可以看到它是否通过了——但如果你无头运行它就不行。 通过这个测试,我们只知道 Cypress 是否可以成功运行我们的命令,而不知道我们是否最终出现在作者的网站上。 所以,我们需要教我们的测试来确定这一点。
使用断言
第二种类型的语句负责对 UI 所需状态的描述——即,某物是否应该存在、可见或不再可见。 Cypress 中的断言基于 Chai 和 Sinon-Chai 断言,这在语法中很明显。
请记住,我们想检查我们是否在作者的个人资料页面上——在这个例子中是我的。 因此,我们需要为此添加一个断言:
// find-author.spec.js it('Find the author Ramona Schwering', () => { // Open the website cy.visit('https://www.smashingmagazine.com'); // Enter author's name in search field cy.get('#js-search-input').type('Ramona Schwering'); // Navigate to author's article cy.get('h2 > a').first().click(); // Open the author's page cy.get('.author-post__author-title').click(); // Check if we're on the author's site cy.contains('.author__title', 'Ramona Schwering').should('be.visible'); });
好的,现在我们已经编写了一个有价值的测试。 所以,是的,恭喜你编写了你的第一个测试......即使它还不完美。
让我们的测试漂亮
即使我们已经成功编写了第一个有意义的测试并在此过程中学习了核心概念,如果它是在拉取请求中提出的,我也不会合并它。 有几件事要做才能让它发光。
慢慢来
赛普拉斯几乎在每个命令中都有一个内置的重试选项,因此您不必等待查看某个元素是否已经存在。 但是,这只是查看 DOM 中是否存在元素,仅此而已。 赛普拉斯无法预测您的应用程序所做的一切,因此如果您仅依赖于此,可能会出现一些问题。
如果用户想查看仍在加载的网站,他们会怎么做? 他们很可能会等到网站的某些部分变得可见(因此,加载),然后与他们进行交互。 在我们的测试中,我们想要精确地模仿:我们想要在开始交互之前等待 UI 的变化。 在大多数情况下,我们会将这种行为限制在我们需要的元素上,因此对这些元素使用断言。
如您所见,我们必须让我们的测试多次等待。 但是,等待太多次也不好。 根据经验,我建议使用断言来检查要与之交互的元素是否已完全加载,作为确定正在测试的网站是否已加载的第一步。
让我们以我们测试的这样一个部分为例。 我添加了一个断言以确保我们的页面已完全加载:
// find-author-assertions.spec.js // Open website cy.visit('https://www.smashingmagazine.com'); // Ensure site is fully loaded cy.get('.headline-content').should('be.visible'); // Enter author's name in the search field cy.get('#js-search-input').type('Ramona Schwering');
继续以这种方式向我们的网站将有加载时间或需要重新呈现的几个元素的所有实例添加断言。 完整的测试文件请查看GitHub仓库中对应的测试。
为了避免落入不稳定测试的陷阱,我想给你最后一个提示:永远不要使用固定的等待时间,例如cy.wait(500)
等。
API 响应是你的朋友
我特别喜欢在测试中使用一种巧妙的等待可能性。 在 Cypress 中,可以使用网络功能 - 在应用程序中等待的另一种有用方法是使用这些功能来处理网络请求。 这样,您可以让测试等待成功的 API 响应。
如果我们以我们的工作流程为例,一个步骤可以充分利用 API 等待的可能性。 我在考虑搜索。 相应的用户故事可能如下:
“作为开发人员,我希望确保我们的搜索结果已完全加载,这样旧结果的文章就不会误导我们的测试。”
让我们把它应用到我们的测试中。 首先,我们需要定义稍后要等待的路由。 我们可以为此使用intercept
命令。 我会搜索请求,带来我需要的数据——在这种情况下是搜索结果。
为了使这个示例简单,我将使用通配符作为 URL。 之后,我将使用别名,以便 Cypress 稍后可以使用此路由。
// find-author-hooks.spec.js // Set the route to work with it('Find the author Ramona Schwering', () => { // Route to wait for later cy.intercept({ url: '*/indexes/smashingmagazine/*', method: 'POST' }).as('search'); // With this alias Cypress will find the request again //...
在赛普拉斯中,所有定义的路由都在测试开始时显示。 所以,我也想把这些intercept
命令放在我测试的开始。
现在,我们可以在断言中使用这个路由别名。 最简单的方法是使用 Cypress 的wait
命令,直接使用前面提到的别名。 但是,单独使用此命令将导致无论结果如何都等待响应。 即使是 400 或 500 之类的错误代码也会被视为通过,而您的应用程序很可能会中断。 所以我建议添加另一个这样的断言:
// find-author-hooks.spec.js // Later: Assertion of the search request's status code cy.wait('@search') .its('response.statusCode').should('equal', 200);
这样,我们可以精确地等待软件的数据、更改等,而不会在应用程序压力过大时浪费时间或陷入问题。 同样,您可以在我的 GitHub 存储库中找到完整的示例文件。
配置赛普拉斯
我遗漏了一个小细节。 如果您仔细查看完整的测试示例,它与我们在本指南中使用的示例略有不同。
// Cypress describe('Find author at smashing', () => { beforeEach(() => { // Open website cy.visit('https://www.smashingmagazine.com'); }); //...
我只用斜线打开 Smashing Magazine 的网站。 这是如何运作的? 好吧,像这样使用这个命令将导航到我们测试的baseUrl
。 baseUrl
是一个配置值,可用作cy.visit()
或cy.request()
命令 URL 的前缀。 在其他值中,我们可以在cypress.json
文件中定义这个值。 对于我们的测试,我们将这样设置baseUrl
:
// cypress.json { "baseUrl": "https://www.smashingmagazine.com" }
荣誉奖:钩子
我还想提一个话题,即使我们的示例测试不适合使用它。 正如在其他测试框架中常见的那样,我们可以通过所谓的生命周期挂钩来定义测试之前和之后发生的事情。 更准确地说,这些存在是为了在一个或所有测试之前或之后执行代码:
// Cypress describe('Hooks', function() { before(() => { // Runs once before all tests }); after(() => { // Runs once after all tests }); beforeEach(() => { // Runs before each test }); afterEach(() => { // Runs after each test }); });
我们想用多个测试填充我们的测试文件,所以我们应该寻找我们想要在它们之前或之后执行的常见步骤。 我们的第一行就是一个很好的例子,即visit
命令。 假设我们想在每个测试之前打开这个网站,我们示例中的beforeEach
挂钩将如下所示:
// Cypress describe('Find author at smashing', () => { beforeEach(() => { // Open website cy.visit('https://www.smashingmagazine.com'); }); //...
例如,我在日常工作中经常使用它来确保我的应用程序在测试之前重置为其默认状态,从而将测试与其他测试隔离开来。 (永远不要依赖以前的测试! )彼此隔离运行测试以保持对应用程序状态的控制。
每个测试都应该能够独立运行——独立于其他测试。 这对于确保有效的测试结果至关重要。 有关这方面的详细信息,请参阅我最近的一篇文章中的“我们曾经共享的数据”部分。 现在,如果您想查看整个测试,请参阅 GitHub 上的完整示例。
结论
在我看来,端到端测试是 CI 的重要组成部分,可以将应用程序的质量保持在高水平,同时减轻测试人员的工作量。 Cypress 是我的首选工具,用于快速、稳定、高效地调试端到端测试,并将它们作为 CI 的一部分与任何拉取请求并行运行。 如果您已经熟悉 JavaScript,那么学习曲线会很平缓。
我希望我能够为您提供一些指导,并为您提供编写 Cypress 测试的起点和一些实用的入门技巧。 当然,所有代码示例都可以在 GitHub 存储库中找到,因此请随意查看。
当然,这只是一个起点; 关于赛普拉斯测试,还有很多东西要学习和讨论——我会给你一些关于接下来要学习什么的建议。 考虑到这一点,祝测试愉快!
资源
- 原始的粉碎示例,Ramona Schwering
本文中示例的 GitHub 存储库。 - 赛普拉斯文档
- “食谱”,赛普拉斯
精选示例、食谱和课程。 - “学习使用 JavaScript 编码:赛普拉斯”(课程),CodeLikeThis
- 编写端到端测试的最佳实践”,Shopware Docs