使用 Cypress 学习出色的端到端测试——学习之后你会爱上编写测试。
欢迎在推特上关注我,我很乐意接受您对话题或改进方面的建议。/克里斯
测试是必不可少的,但我们使用的测试工具却不尽如人意,导致我们编写和运行测试的频率远低于预期。为什么我们不能拥有一个优秀的测试工具呢?其实,有了 Cypress,一切皆有可能。
简而言之:未来的测试工具已经到来。听起来像个梦,但请继续阅读,我相信读完这篇文章后您一定会认同我的观点。Cypress——一款为人类打造的测试运行工具。
参考
- Cypress 如何控制浏览器如果您想了解更多关于 Cypress 如何处理浏览器的信息,那么这篇文章很适合您。
- Cypress 的工作原理:这篇文档很好地解释了 Cypress 的底层工作原理。
- Angular + Cypress 代码库。您可以轻松地使用 Cypress 测试,并在 Vue 或 React 中构建类似的应用程序,这就是 Cypress 的魅力所在。
- 安装 Cypress
- 使用 Cypress 编写你的第一个测试
- 测试策略在测试时,最好先考虑一下要采用的方法。
什么
Cypress 是一款测试工具,能够显著提升您的测试体验。它提供以下功能:
- 时间旅行功能会在您运行测试时拍摄测试快照。这样,您可以轻松地将鼠标悬停在测试中执行的每个命令上。
- 调试功能:可直接使用 Chrome DevTools 等熟悉的工具进行调试。我们清晰易懂的错误信息和堆栈跟踪信息,让调试速度飞快。
- 实时重载:每当您对测试进行更改时,Cypress 都会自动重载。您可以在应用程序中实时查看命令的执行情况。
- 自动等待,无需在测试中添加等待或睡眠。Cypress 会自动等待命令和断言执行完毕后再继续执行——告别异步地狱。
- 间谍、桩和时钟,用于验证和控制函数、服务器响应或计时器的行为。您在单元测试中喜爱的这些功能,现在触手可及。
- 结果稳定可靠。我们的架构不使用 Selenium 或 WebDriver。告别不稳定,体验快速、稳定、可靠且无故障的测试。
- 网络流量控制,无需服务器参与即可轻松控制、模拟和测试极端情况。您可以根据需要模拟网络流量。
- 屏幕截图和视频,查看失败时自动拍摄的屏幕截图,或在无头运行时拍摄的整个测试套件的视频。
为什么
如今,测试似乎并未得到应有的重视。我们通常拥有界面精美的集成开发环境(IDE)来编写代码,但编写和运行测试却感觉十分匮乏,仿佛只是事后才想起来的。我们需要测试来确保我们对所构建的产品充满信心,但我们也理应享有使用一流工具的权利。
端到端测试的一个问题是稳定性差。Cypress 提供了一款优秀的测试运行器,它稳定性极佳,既可以运行单元测试,也可以运行端到端测试,未来已来。
安装 Cypress
你想将 Cypress 作为依赖项安装到你的项目中。请确保npm init已经运行了安装命令。我通常选择默认的智能安装选项:
npm init -y
然后使用以下命令安装 Cypress:
npm install cypress
也可以使用 Yarn:
yarn add cypress --dev
您的可执行文件现在位于:
./node_modules/.bin/
起始柏树
你想打电话cypress open启动 Cypress。你可以通过以下方式之一进行操作:
./node_modules/.bin/cypress open$(npm bin)/cypress opennpx cypress open适用于 NPM 版本 > 5.4,或npx单独安装yarn run cypress open
我们将选择npx cypress open:
这也会弹出一个窗口,看起来像这样:
根据标题文本,它已添加了测试样本。让我们来看一下项目结构。
如上所示,我们得到了一个cypress包含integration子文件夹的目录,如下所示:
-| cypress/
---| integration
------| examples
查看文件夹内容examples,我们可以看到它包含了我们可能想知道如何操作的所有信息,例如:
- 写作测试
- 模拟 API
- 不同的断言
- 别名
还有更多精彩内容。这太棒了,我们以后有很多理由再来,但首先我们得学会爬行。那么,从哪里开始呢?
我们的第一次测试
让我们sample_spec.js在该cypress/integration文件夹下创建一个新文件:
- | cypress
---| integration
------| sample_spec.js
文件创建完成后,它会被仍在运行的测试运行程序检测到:
让我们添加以下内容sample_spec.js:
describe('My First Test', function() {
it('Does not do much!', function() {
expect(true).to.equal(true)
})
})
让我们保存内容,然后在测试运行器中点击测试。这样做应该会弹出以下窗口:
我们通过了测试 :)
我们可以通过修改代码轻松使其失败:
describe('My First Test', function() {
it('Does not do much!', function() {
expect(true).to.equal(false)
})
})
代码保存后,运行程序会立即显示以下内容:
真正的考验
让我们来写一些更实际的东西。目前为止,我们测试了true`vs` true。虽然严格来说这只是一个测试,但我们想展示 Cypress 的真正实力。所以,让我们在网页上测试一下它的功能。
我们的总体思路如下:
- 访问网页。
- 查询元素。
- 与该元素互动。
- 对页面上的内容进行陈述。
访问
Cypress 提供了许多辅助工具,使上述操作变得非常容易。首先,让我们创建一个包含测试用例的测试套件。让我们在文件夹page_spec.js下创建一个文件。integration
-| cypress/
---| integration/
------| page_spec.js
现在,添加以下内容:
describe('My First Test', function() {
it('Visits page', function() {
cy.visit('https://example.cypress.io')
})
})
我们可以看到,我们使用全局对象cy和辅助方法visit()来跳转到某个页面。
和往常一样,Visits page测试题出现了,我们可以点击它。现在我们看到的是以下用户界面:
在左侧,我们看到测试套件、测试以及我们当前正在执行的操作,VISIT而在右侧,我们看到执行该操作的结果,也就是我们导航到的网页。
查询元素
现在我们来查找一个元素。有很多辅助函数可以帮助你查找元素,但我们这里通过内容来查找这个元素:
cy.contains('type')
让我们把这段代码添加到代码中,这样我们的测试就变成了:
describe('My First Test', function() {
it('Visits page', function() {
cy.visit('https://example.cypress.io')
cy.contains('type')
})
})
我们保存一下。请注意我们的测试运行程序现在是如何输出的:
上方左侧显示的是我们cy.contains()创建的CONTAINS操作。右侧显示的是与我们的设置匹配的元素被高亮显示cy.contains()。
相互影响
下一步是与我们的用户界面进行交互,特别是与我们找到的元素进行交互,所以我们点击它,就像这样:
cy.contains('type').click()
保存一下。你应该会看到以下结果:
点击我们的type元素后,它被展开,现在显示了很多我们可以进行断言的内容。
断言
好的。关于这一点,我们可以提出不止一个观点:
- URL,我们的 URL 实际上在我们点击此元素后发生了变化。
- 内容,正在显示新内容,让我们确认它是正确的内容。
要对 URL 进行断言,我们可以使用辅助函数cy.url().should('include', '<something>')。这里我们使用了多个辅助函数:
cy.url()这样可以帮助我们获取URL.should()这是一个断言。include这是一个关键字,它告诉我们应该将 URL 的哪一部分与我们期望的输出进行匹配。
在本例中,我们希望表达式为:
cy.url().should('include', '/commands/actions')
这意味着我们希望 URL包含 /commands/actions
那么其他类型的断言呢,例如输入元素?为此,我们可以使用cy.get()辅助函数,如下所示:
cy.get('.action-email')
.should('have.value', '<some value>')
以上我们是通过 CSS 类获取电子邮件的。
让我们将上述断言添加到测试中,使测试现在如下所示:
describe('page test - suite', () => {
it('Visits page', function () {
cy.visit('https://example.cypress.io')
cy.contains('type').click()
cy.url().should('include', '/commands/actions')
cy.get('.action-email')
.should('have.value', '')
})
})
让我们保存这段新代码。重新运行测试后,您应该会在用户界面中看到以下更新。
我们可以看到,它能够正确地对 URL 和元素进行断言。
让我们尝试更改输入元素的内容,以确保它能够正确更新(是的,我在我用过的每个 SPA 框架中都搞砸了这一点 :) )。
要使用辅助函数向输入元素中输入内容.type(),请像这样:
cy.get('.action-email')
.type('fake@email.com')
.should('have.value', 'fake@email.com')
保存此设置并重新运行测试后,结果如下:
如上所示,它将内容输入到我们的文本元素中,所以是的,我们没有搞砸我们在 SPA 中使用的双向单向数据流(或者我们本来可以使用原生 JS 的 ;) )。
调试
好的,我们上面学习了一些关于如何编写和运行测试的知识,而且大部分测试都通过了。但是,如果出现问题怎么办?为此,我们可以使用强大的调试支持,即时间旅行和快照功能。对于执行的每个操作,您都可以自由地在不同的快照之间来回切换。让我们来演示一下。
除了快照功能外,我们还可以使用以下两个附加命令:
cy.pause()这使我们能够在测试过程中暂停在特定位置。cy.debug()
cy.pause()
例如,像这样在代码中添加:
describe('page test - suite', () => {
it('Visits page', function () {
cy.visit('https://example.cypress.io')
cy.contains('type').click()
cy.url().should('include', '/commands/actions')
cy.pause()
cy.get('.action-email')
.should('have.value', '')
cy.get('.action-email')
.type('fake@email.com')
.should('have.value', 'fake@email.com')
})
})
我们编写代码使测试执行停止,如下所示:
从上图可以看出,我们还得到了一个控制面板,它指示我们当前处于什么状态(暂停),一个播放按钮用于恢复测试运行,step右侧还有一个按钮,允许我们逐步执行测试中的每一行代码。
cy.debug()
如果我们替换掉cy.pause()for 循环呢cy.debug()?嗯,情况略有不同。如果你打开开发者工具,代码会在debugger类似这样的行停止:
如果你打开控制台,你会看到一些额外的日志记录,如下所示:
所以这是获取开发者工具来帮助您调试问题的另一种方法。
嘲讽
模拟是一个很有意思的话题。之所以有趣,是因为它关系到我们想要编写什么样的测试。如果我们模拟后端,就能真正实现端到端测试吗?答案是否定的,但模拟整个后端的某些端点仍然很有价值,它可以确保我们能够轻松地测试某些场景。那么,让我们来探讨一下如何实现这一点。
我们可以通过两种方式进行嘲讽:
- 内联代码模拟,这意味着我们将拦截某个路由调用,并用一个 JavaScript 对象作为响应。
- Fixtures,这本质上与上述相同,但不同之处在于,我们不是将响应作为 JavaScript 代码,而是指向一个 JSON 文件。
内联代码模拟
首先,我们需要调用 `Cypress.commit()` cy.server(),这将告诉 Cypress 我们允许进行模拟。接下来,我们告诉它要模拟哪个端点,如下所示:
cy.route({
method: 'GET',
url: 'http://localhost:3000/products',
response: [{
id: 1,
title: 'Mocked Tomato'
}]
})
以上内容已经很容易理解,但我们还是解释一下:
- 方法,这是我们要监听的HTTP方法。
- url,这只是网址,我们可以像上面那样进行精确匹配,也可以使用通配符匹配。
* - 响应,在这里我们可以指定想要的响应,而不是让实际的 API 调用继续进行。
不过,这样感觉不太好。当然,它确实有效,但总觉得我们可以做得更好。我们可以,通过固定装置。
固定装置
关于这件事,我们需要了解以下几点:
- fixtures 是 JSON 文件
- Cypress 会自动在
fixtures目录中查找 fixtures。 - 您可以根据需要创建任意数量的子目录,Cypress 都能找到它们。
让我们来展示一下它的样子。假设有以下目录结构:
-| cypress/
---| fixtures/
------| heroes/
---------| list.json
以及以下内容list.json:
[{
"id" : 1,
"title" : "Sir Mockalot"
}]
现在我们可以指示 Cypress 使用上述 JSON 文件,如下所示:
cy.fixture('heroes/list.json').as('heroesList')
cy.route('GET', 'http://localhost:3000/products', '@heroesList');
调用 `get_json_file_url`cy.fixture()指定了 JSON 文件相对于目标 URL 的位置/fixtures,我们通过创建一个别名 heroesList来查找,该别名可以在下一行中使用。调用 `get_json_file_url` 的功能cy.route()与之前相同,但我们需要输入更少的代码。它首先接受一个 HTTP 动词,然后是要模拟的 URL,最后是我们的别名。注意,我们在别名前加上了前缀@。
试想一下,如果能够具体说明你可能需要的所有场景,那该有多棒!
最关键的问题是:我们的代码应该放在哪里?答案是:哪里需要它就放在哪里。它应该放在调用端点之前。假设上述端点在页面加载时被调用,那么以下代码就是正确的:
cy.fixture('heroes/list.json').as('heroesList')
cy.route('GET', 'http://localhost:3000/products', '@heroesList');
cy.visit('http://localhost:4200')
cy.server()
将 Cypress 添加到您的 SPA 应用中
现在,将 Cypress 添加到任何单页应用程序 (SPA) 都非常简单,它就是这么好用。我们以 Angular 为例,但您也可以将其应用于 React、Vue 或 Svelte。我们将执行以下操作:
- 搭建一个 Angular 项目
- 安装Cypress
- 设置package.json
- 编写一些测试
- 启动一切
搭建我们应用程序的框架
在 Angular 中,这就像调用以下代码一样简单:
ng new <name of my project>
cd <name of my project>
安装 Cypress
使用以下命令即可安装 Cypress:
npm install cypress --save-dev
设置 package.json
我们希望能够同时启动我们的应用程序和 Cypress。有很多方法可以实现这一点,但一种常用的方法是使用 Cypress 库concurrently,我们可以使用以下命令安装它:
npm install concurrently
package.json现在让我们在各自的部分中设置一个任务scripts,如下所示:
"cypress": "concurrently \"ng serve\" \"cypress open\" \"json-server --watch db.json\""
上面展示了我们如何使用 ` concurrentlyng start` 启动 Angular 应用ng server,然后使用 `ng start` 启动 Cypress cypress open,最后使用 `ng start` 启动 API json-server --watch db.json。如果您有真正的 API,请使用相应的命令来启动它。我们只是使用 ` json-serverng start` 来模拟我们拥有一个真正的 API。
编写一些测试
好的,让我们根据以下应用程序编写一些测试:
这是一个简单的待办事项应用,我们可以:
- 添加项目
- 更新项目
- 删除项目
将项目添加到列表
在这个测试中,我们首先在文本框中输入一个值。然后点击一个按钮添加该项,最后断言用于输入的文本框已被清空,并且添加的项确实存在于列表中。
it('should add Hulk to list', () => {
cy.visit("http://localhost:4200")
cy.get(".new-hero")
.type("Hulk")
.should("have.value", "Hulk")
cy.get(".new-hero-add")
.click()
cy.get(".new-hero")
.should("have.value", "");
cy.get(".hero-input")
.eq(2)
.should("have.value", "Hulk");
})
断言列表中有 2 个项目
这只是为了确保列表中有 2 个元素。我们获取列表元素的引用并检查其长度是否为 2 2。
it('should have list with length 2', () => {
cy.visit('http://localhost:4200')
cy.get(".hero-input")
.its('length')
.should("eq", 2);
})
更新列表中的项目
在这里,我们更改列表中的一个项目,然后单击以更新该项目,最后断言该项目已更新。
it('should update our item' , () => {
cy.visit("http://localhost:4200")
cy.get(".hero-input")
.eq(1)
.should("have.value", "Captain Marvel")
.type("s")
;
cy.get('.hero-update')
.eq(1)
.click()
cy.get(".hero-input")
.eq(1)
.should("have.value", "Captain Marvels");
})
删除项目
这段代码的作用是找到列表中特定索引处的元素。然后,我们点击相应的删除按钮。最后,我们进行验证,确保该元素已从列表中移除。
it('should remove our item', () => {
cy.visit("http://localhost:4200");
cy.get(".hero-remove")
.eq(1)
.click();
cy.get(".hero-input")
.its("length")
.should("eq", 1);
})
启动所有程序
启动所有程序时,我们调用:
npm run cypress
要查看 Angular 项目的完整源代码以及相应的测试,请查看以下仓库。
TypeScript
目前大多数主流单页应用程序 (SPA) 框架都支持 TypeScript。但 TypeScript 并非总是适合您的项目,是否添加完全取决于您。请记住,您可以根据实际情况逐步添加 TypeScript 的部分功能。
那么,在我的 Cypress 测试中添加 TypeScript 有多容易呢?
非常简单,因为 Cypress 自带 TypeScript 类型定义。你只需要一个tsconfig.json包含以下内容的文件:
{
"compilerOptions": {
"strict": true,
"baseUrl": "../node_modules",
"target": "es5",
"lib": ["es5", "dom"],
"types": ["cypress"]
},
"include": [
"**/*.ts"
]
}
这样设置好之后,自动完成功能就能正常工作了,就像这样:
屏幕截图
最后,我们来聊聊一个非常棒的功能——免费截图。你只需要调用一下 `.` 函数cy.screenshot()。如果你把它放在生命周期方法中,比如beforeEach()`.`,它就会为该测试套件中的每个测试生成一张截图。
屏幕截图screenshots默认保存在该目录中。
下面我们给出一个示例,其中我们调用了以下函数list_spec.js:
describe('', () => {
beforeEach(() => {
cy.screenshot();
})
})
概括
这篇文章有点长,但希望您已经了解了 Cypress 的强大功能。说实话,我只展示了它的一小部分。作为一名开发者,您不仅需要一个优秀的 IDE 来编写代码,还需要一个出色的测试运行器。我保证,给 Cypress 一个机会,您很快就会主动要求编写测试。
文章来源:https://dev.to/itnext/great-e2e-testing-with-cypress-3ak1

















