端到端测试非常棒,因为它可以模拟用户的体验。您可能需要大量的单元测试才能获得良好的覆盖率(例如测试函数是否返回预期值),但您可以编写单个端到端测试,使其像真实用户一样,一次测试应用程序的多个部分。这是一种非常经济的应用程序测试方法。
Cypress 是一款比较新的测试运行器,它具有一些特性可以简化端到端测试。它具有自动等待元素(如果尝试获取找不到的元素)、等待 Ajax 请求、清晰显示测试结果以及易于使用的 API 等功能。
注意:Cypress 既是测试运行器,也是一项付费服务,它会记录您的测试,以便您稍后回放。本文重点介绍测试运行器,您可以免费使用它。
安装 Cypress
可以使用 npm 轻松安装 Cypress.io。在您的终端中输入以下命令,为您的项目安装它
npm install --save-dev cypress
如果一切正常,您应该在终端中看到如下所示的输出

现在,让我们编写一些测试来了解它的工作原理!
注意:Alex Haid 提到,在 Windows 上,他最终必须在 npm 安装后运行 .\node_modules\.bin\cypress.cmd install
以完成安装并看到上面显示的 3 个勾号。
为 CSS-Tricks 设置测试
我们将为 CSS-Tricks 编写一些测试,因为它是我们都熟悉的内容……也许这可以帮助 Chris 在添加功能或重构代码时避免任何**回归**(即更改网站的一个部分导致另一个部分出现问题)。😜
我将在该项目的目录中开始。我在我的项目目录中创建了一个名为 testing-css-tricks
的新目录。通常,您的 Cypress 测试将位于要测试的项目的目录结构中。
默认情况下,Cypress 期望集成测试位于项目根目录下的 cypress/integration
中,因此我将创建该文件夹来保存我的测试文件。以下是在终端中执行此操作的方法
mkdir cypress
mkdir cypress/integration
不过,您**不必**使用此默认位置。您可以通过在项目根目录中创建一个 cypress.json
配置文件 并将 integrationFolder
键设置为所需的任何路径来更改此设置。
测试:检查页面标题
让我们从一些非常简单的事情开始:我想确保网站名称在页面标题中。

describe 函数
我在 cypress/integration
中创建了一个名为 sample-spec.js
的文件。在该文件中,我将通过调用 describe
来启动测试。
describe('CSS-Tricks home page', function() {
});
describe
接受两个参数:一个字符串,我认为它是测试语句的“主体”,以及一个回调函数,该函数可以运行所需的任何代码。回调函数也应该调用 it
,它告诉我们希望在此测试中发生什么并检查结果。
it 函数
describe('CSS-Tricks home page', function() {
it('contains "CSS-Tricks" in the title', function() {
});
});
it
函数具有相同的签名:它接受一个字符串和一个回调函数。这次,字符串是测试语句的“动词”。我们在 it
回调函数内部运行的代码最终应该将此测试的**断言**(我们期望的结果)与现实进行比较。
此 describe
回调函数可以包含对 it
的多次调用。最佳实践建议每个 it
回调函数都应测试一个断言。
设置测试
不过,我们有点超前了。在我们的 describe
调用中,我们已经明确表示打算测试主页,但我们并没有在主页上。由于此 describe
回调函数中的所有测试都应该测试主页(否则它们应该放在其他地方),因此我们可以在 describe
回调函数中的 beforeEach
中导航到该页面。
describe('CSS-Tricks home page', function() {
beforeEach(function() {
cy.visit('https://css-tricks.org.cn/');
});
it('contains "CSS-Tricks" in the title', function() {
});
});
beforeEach
的作用正如其名。传递给它的回调函数中的任何代码都会在同一作用域中的每个测试之前执行(在本例中,只是它下面的单个 it
调用)。您可以访问一些其他函数,例如 before
、afterEach
和 after
。
您可能想知道为什么不在这里使用 before
,因为我们将使用此块中的每个断言测试相同的页面。beforeEach
和 afterEach
比其一次性对应函数更常用,因为您希望确保在每个测试开始时都处于**一致的状态**。
假设您编写了一个测试来确认可以键入搜索字段。很好!假设您随后编写了一个测试来确保搜索字段为空。失败!由于您在之前的测试中键入了搜索字段而没有清理,因此您的第二个测试将失败,即使网站的功能完全符合您的预期:首次加载时,搜索字段为空。如果您在每个断言之前都加载了页面,那么就不会出现问题,因为每次都会处于一个新的状态。
驱动浏览器
上面示例中的 cy.visit()
等效于用户在地址栏中点击、键入 https://css-tricks.org.cn/
并按回车键。它将在 Web 浏览器中加载此页面。现在,我们准备编写断言。
describe('CSS-Tricks home page', function() {
beforeEach(function() {
cy.visit('https://css-tricks.org.cn/');
});
it('contains "CSS-Tricks" in the title', function() {
cy.title().should('contain', 'CSS-Tricks');
});
});
标题断言
cy.title()
会生成页面标题。我们将其与 should()
链接,后者会创建一个断言。在此示例中,我们向 should()
传递两个参数:一个链接器和一个值。对于链接器,您可以从几个不同的 JavaScript 测试库中的断言中选择。contains
来自 Chai。(Cypress 文档中 提供了它支持的所有断言的便捷列表。)
有时,您会发现多个断言可以实现相同的功能。您的目标应该是使整个测试尽可能接近英文句子。使用在上下文中最有意义的那个。
在本例中,我们的断言表示:标题应包含“CSS-Tricks”。
运行我们的第一个测试
现在,我们已经准备好运行测试了。从项目根目录使用以下命令
$(npm bin)/cypress open
由于 Cypress 不是全局安装的,我们需要从该项目的 npm 二进制文件中运行它。$(npm bin)
将替换为此项目的 npm 二进制文件路径。我们从那里运行 cypress open
命令。如果一切正常,您将在终端中看到此输出

…并且您将看到一个带有测试运行器 GUI 的网页浏览器

点击“运行所有规范”按钮开始运行您的测试。这将在新的浏览器窗口中显示您的测试结果。左侧是您的测试及其步骤。右侧是测试“浏览器”。

这带我们了解了 Cypress 的另一个酷炫功能。端到端测试的一个问题是**了解测试结果的可视性**。每个测试运行器都会为您提供“通过”或“失败”,但它们在显示导致失败的原因方面做得非常糟糕。您知道什么**没有**发生(您的测试断言),但要找出什么**确实**发生了则比较困难。过去,我曾尝试在测试过程中在各个点对测试浏览器进行截图,但这很少能给我所需的答案。这就像用 console.log
疯狂地调试问题一样,是自动化测试的等价物。
使用 Cypress,我可以点击左侧测试的每个步骤,以查看右侧该点页面的状态。

测试:检查页面上的元素
接下来,我们将检查一个我们希望确保在页面上的元素。页面应该始终包含徽标,并且应该是可见的。

由于我们正在测试同一个页面,因此我们将向 describe
回调添加一个新的 it
调用。
it('has a visible star logo', function() {
cy.get('.icon-logo-star').should('be.visible');
});
我们仍然像以前一样从主页进行测试,因为 cy.visit()
调用发生在每个测试之前。此测试使用 cy.get()
获取我们想要检查的元素。它的工作方式有点像 jQuery:您向它传递一个 CSS 选择器字符串。然后,我链接一个 should()
调用并检查可见性。
这里需要注意两点:首先,如果此元素是异步加载的,cy.get()
会自动等待 defaultCommandTimeout
以查看元素是否显示。(该默认值为四秒,可以在 cypress.json 中更改。)其次,如果您添加该测试并保存文件,您的测试将自动重新运行新测试。这使得迭代测试变得非常快速和容易。
以下是结果

测试:确保导航响应式
我们将尝试使用此测试做一些稍微复杂的事情。我想确保响应式菜单在较小的视口中可用。否则,用户可能无法正确浏览网站。

我们仍在测试主页,因此我将在此测试中写入相同的 describe
回调。不过,我正在测试一个**稍微**不同的场景,因此我将嵌套另一个 describe
调用来指示我的测试的具体情况并设置这些情况。
describe('CSS-Tricks home page', function() {
// Our existing tests and the beforeEach are here
describe('with a 320x568 viewport', function() {
});
});
在 320px 宽度下测试
在这里,我决定在 320px 宽度下测试响应式导航菜单,但了解默认测试视口也很有用。您可以点击测试运行器中的任何测试,并在浏览器窗格上方查看视口宽度。

1000×660
是默认的视口大小。您可以在 cypress.json 配置文件中更改此设置。我们将首先编写在 320px 宽度下运行的测试。然后,我们将为几个不同的视口复制该测试。
要仅为此测试更改视口,我们可以调用 cy.viewport()
。
describe('with a 320x568 viewport', function() {
beforeEach(function() {
cy.viewport(320, 568);
});
});
现在,我们将向嵌套的 describe
回调中添加一个 it
调用。现在我们已设置视口,此测试将与徽标测试非常相似。
it('has a visible mobile menu toggle', function() {
cy.get('#mobile-menu-toggle').should('be.visible');
});
在 1100px 宽度下测试
我将在 1100px 处运行相同的测试,以确保响应式菜单仍然存在。我认为这是应该具有菜单的最大宽度,因此我想确保它确实存在。
describe('with a 1100x660 viewport', function() {
beforeEach(function() {
cy.viewport(1100, 660);
});
it('has a visible mobile menu toggle', function() {
cy.get('#mobile-menu-toggle').should('be.visible');
});
});
糟糕!这里发生了什么?

由于测试只测试了一件事,我们对发生的事情有了一个很好的了解:响应式菜单在 1100px 视口宽度下不可见。测试的反馈也给了我们一些有用的信息。
“重试超时:预期 ‘
此元素 <button#mobile-menu-toggle.button.button-header.mobile-menu-toggle>
不可见,因为其父元素 <div.menu-toggle-area>
具有 CSS 属性 display: none
。
Cypress 等待了 defaultCommandTimeout
时间,以使移动菜单切换按钮可见,但它没有可见。它不被认为是可见的,因为父元素具有 display: none
。说得通。
以下是 Cypress 提供给我们的,而其他测试运行器没有提供的功能:检查失败状态的机会。

当我点击其中一个测试步骤时,我可以在右侧看到该步骤在浏览器中运行时页面的状态,但我也会在控制台中获得输出。(打开 Chrome 开发者工具并检查控制台以查看。)
在这种情况下,甚至没有必要。很容易看出该页面在此宽度下没有响应式菜单。

在理想的现实世界场景中,我将首先编写测试以反映我想要的内容(在这种情况下,是 1100px 视口宽度下的响应式菜单)。然后,我将返回并更改代码以修复我的测试失败。换句话说,我将确保响应式菜单在 1100px 处显示。这称为**测试驱动开发**。
但是,在这种情况下,由于我正在测试一个实时网站,因此我将重写测试以适合网站已有的功能。如果您正在向现有网站添加测试以防止出现回归,则可能会使用更类似于此方法的方法,在该方法中,您编写测试以反映现有功能。
响应式菜单在高达 1086px 的宽度下可见,因此我们将此测试的视口宽度更改为 1085px。我们希望确保更改传递给 describe
的字符串,以正确反映新的宽度。
describe('with a 1085 viewport', function() {
beforeEach(function() {
cy.viewport(1085, 660);
});
it('has a visible mobile menu toggle', function() {
cy.get('#mobile-menu-toggle').should('be.visible');
});
})
现在,我们有一个通过的测试!

测试:搜索
对于像 CSS-Tricks 这样内容丰富的网站,搜索功能至关重要。我们将将其测试分为两个部分:首先,确保请求发出;其次,确保结果显示在页面上。
但是,在执行这两个操作之前,我们必须触发搜索。
让我们在主页 describe
回调中添加一个 describe
调用,以指示我们正在测试搜索。
describe('site search', function() {
});
我们需要使用回调调用 beforeEach
,该回调将触发搜索。为了触发搜索,我们将使用 Cypress API 以与用户相同的方式 与页面交互。我们将首先在搜索字段中输入内容。然后,我们将按下键盘 Enter 键。
beforeEach(function() {
cy.get('.search-field').type('flexbox{enter}');
});
如果您查看 type
方法的文档,您会看到 {enter}
是一个触发 Enter 键按下操作的特殊序列。这应该会提交我们的搜索。
开始进行实际测试!
检查 URL
我们的搜索应该加载一个新的页面,地址为 https://css-tricks.org.cn/?s=<search-term>
。让我们调用 it
it('requests the results', function() {
});
为了确保请求了该页面,我们现在将检查 URL 中的搜索词,因为我们已经触发了搜索。
cy.url().should('include', '?s=flexbox');
问号在 URL 中启动一个查询字符串。由于 CSS-Tricks 始终将搜索参数放在查询字符串的开头,因此我们可以查找以 ?
开头的子字符串。该网站的搜索词参数为 s
。通过确认该参数在 URL 中是否存在,并具有我们搜索的值,我们就知道已发出了搜索请求。
确认我们有结果
为了确认结果,我们实际上并没有测试首页。我们测试的是结果页面。由于该页面是我们顶级 describe
调用的起点,我们将创建一个新的顶级 describe 调用来测试搜索结果页面。
describe('Search results page', function() {
});
在实际应用中,我们可能会将这些测试代码拆分到单独的文件中。这样做可以更容易地找到测试用例,同时也能提高开发效率。Cypress 会在你保存测试文件更改时重新运行测试。如果你正在处理单个页面,并且将测试拆分到不同的文件中,你就可以只运行该文件中的测试。由于测试需要一些时间才能运行,因此在你进行更改或添加新的测试时,这可以让你迭代更加紧凑。
现在,我们需要进入这个页面。我们将在新的 describe
调用的回调函数内部使用 beforeEach
中的 visit
函数,在测试之前导航到该页面。
beforeEach(function() {
cy.visit('https://css-tricks.org.cn/?s=flexbox');
});
这种方法可以工作,但是,由于我们要测试的所有页面都在 CSS-Tricks 上,如果我们不必在每个测试中都重复协议(https
)和域名(css-tricks.com
)会更好。这样可以使我们的测试更加DRY(避免重复)。
幸运的是,我们可以通过 cypress.json 文件中的配置来实现这一点,使用 baseUrl
属性。以下是带有 baseUrl
设置的 cypress.json 文件示例。
{
"baseUrl": "https://css-tricks.org.cn/"
}
确保此文件位于项目的根目录下。任何设置都将覆盖 Cypress 的默认设置。
有了此配置,我们就可以从任何 visit
调用中移除 URL 的这部分内容。
describe('CSS-Tricks home page', function() {
beforeEach(function() {
cy.visit('/');
});
// ... shortened for brevity
});
describe('Search results page', function() {
beforeEach(function() {
cy.visit('/?s=flexbox');
});
});
我们准备检查页面是否包含结果。以下是 it
调用
it('displays search results', function() {
});
通过检查页面,我发现每个搜索结果都是一个 li
元素,位于具有 search-grid-list
类的元素内部。我将以此作为测试选择器的基础。
it('displays search results', function() {
cy.get('.search-grid-list li').should('exist');
});
此测试将告诉我们搜索结果页面上至少有一个结果。对于此演示来说,这已经足够了,但在实际测试中,我们需要某种方法来控制搜索返回的结果。如果我们针对站点的本地副本进行测试而不是实时站点,则会更容易。我们无法控制实时 CSS-Tricks 站点上的内容,因此我们无法准确预测任何搜索词将返回什么结果。在此演示中,我假设搜索词 flexbox
始终会在站点上返回至少一个结果。
让我们检查结果

接下来是什么…
现在,我们有了使用 Cypress 实现一些测试的良好基础。我们学习了
- 如何组织测试
- 如何在测试浏览器中访问页面
- 如何检查标题
- 如何在不同的视口尺寸下进行测试
- 如何检查浏览器中的 URL
- 如何获取元素并对其进行测试
- 如何与表单交互
我们没有涉及 Cypress 最酷的方面之一:它允许你处理 Ajax 请求的方式。这对于单页应用程序非常有用。Cypress 可以位于服务器和你的应用程序之间。这使它能够在发出请求时等待响应返回。
你还可以控制响应。我之前提到过,如果我们能够控制搜索返回的结果会更好。如果 CSS-Tricks 搜索使用 Ajax 加载结果,我们可以轻松地提供一组静态结果并测试它们是否在页面上正确呈现。
Cypress 拥有非常好的文档。如果你准备好尝试一些 Ajax 相关的内容,请查看 Cypress 关于网络请求的指南。
无论你接下来做什么,都请利用你现在掌握的知识,将其应用到当前项目中,实现一些自动化测试。随着你的应用程序或网站变得越来越复杂,引入回归的可能性会急剧增加。你不想过于专注于引入新功能,最终导致破坏旧功能。自动化测试是帮助你避免这种情况的一种好方法,但它不会强迫你每次都手动测试每个功能。
你是否因为想要成为一名 Web 开发者而来到 CSS-Tricks?如果是这样,我希望能提供帮助。我撰写了像这样技术教程,但我也涵盖了您进行转换所需的其它技能。如果你只关注技术学习,那么你可能会错过这些内容。
现在,我每周都会向注册我邮件列表的 CSS-Tricksters 提供 四次免费的指导课程。每个人都会收到很棒的文章和资源,帮助你成为一名 Web 开发人员,并且在我的时间安排范围内,尽可能多的人会获得个性化的实时建议,关于如何在职业转换中迈出下一步。
如果你不能用它来自动化非 Chrome 浏览器,那么它在现实世界中有什么用?用户使用 Safari、Firefox、IE、Edge 等应用程序——如果所有这些都是盲点……你没有做好你的工作,因为“你无法确定产品的质量”。
我使用 WebDriver 不是因为我喜欢它,而是因为它允许我为所有用户的浏览器进行测试。坦率地说,我不理解为什么只在一个浏览器上进行测试。如果有一天 Cypress 决定支持其他浏览器,我将是第一个迁移的人。
https://github.com/cypress-io/cypress/issues/310#issuecomment-354925454
他们正在努力。请参见 https://github.com/cypress-io/cypress/pull/1359
边际?我不这么认为……如果你收集所有边缘情况,那么将会有很大一部分用户无法使用你的产品。换句话说——你无法“转化”他们——这会导致业务损失。现在,考虑到有些产品拥有企业客户(银行、政府),他们仍然使用遗留软件,并且被 IT 部门阻止升级。我不能无视他们,他们也是我的用户。
我在 Safari 的 localStorage、卸载事件等方面遇到了问题……IE 11 存在奇怪的问题,可能应该被淘汰了。移动浏览器被制造商有意地设置为不同的行为……
所以,我不同意。我认为 Cypress 非常棒,并且自从我上次审查以来有所改进……我希望我能够负担得起使用它。
顺便说一句,我注意到 WebDriver 协议 (https://www.w3.org/TR/webdriver1/) 正在获得关注……它可能能够解决困扰这种测试方式的一些问题。你怎么看?