Skip to main content
Cypress应用

Cypress App 简介

tip

这是理解如何使用 Cypress 进行测试的最重要指南。请仔细阅读并理解。如有疑问请提出,以便我们改进。

info
你将学到
  • Cypress 遵循的规则,助你高效测试应用
  • 如何在 Cypress 中查询元素
  • Cypress 如何使用命令链和异步命令
  • 如何与元素交互并进行断言

Cypress 可以很简单(有时)

简洁性在于用更少的代码完成更多工作。看个例子:

describe('文章资源', () => {
it('创建新文章', () => {
cy.visit('/posts/new') // 1.

cy.get("input.post-title") // 2.
.type("我的第一篇文章"); // 3.

cy.get("input.post-body") // 4.
.type("你好,世界!"); // 5.

cy.contains("提交") // 6.
.click(); // 7.

cy.get("h1") // 8.
.should("contain", "我的第一篇文章");
});
});

你能读懂这段代码吗?它大致是这样的:

note
  1. 访问 /posts/new 页面(或挂载 PostBuilder 组件)
  2. 查找类名为 post-title<input>
  3. 输入"我的第一篇文章"
  4. 查找类名为 post-body<input>
  5. 输入"你好,世界!"
  6. 查找包含文本 提交 的元素
  7. 点击它
  8. 查找 h1 标签,确保它包含文本"我的第一篇文章"

这个测试相对简单,但考虑一下它覆盖了多少客户端和服务端代码!

本指南后续将探讨使这个示例工作的 Cypress 基础知识。我们将揭秘 Cypress 遵循的规则,助你高效测试应用,模拟用户操作,并在必要时使用快捷方式。

查询元素

Cypress 类似 jQuery

如果你用过 jQuery,可能习惯这样查询元素:

$('.my-selector')

在 Cypress 中,查询元素的方式相同:

cy.get('.my-selector')

事实上,Cypress 内置了 jQuery, 并暴露了许多 DOM 遍历方法,让你能用熟悉的 API 轻松处理复杂 HTML 结构。

// 每个 Cypress 查询都等同于对应的 jQuery 方法
cy.get('#main-content').find('.article').children('img[src^="/static"]').first()
tip
核心概念

Cypress 利用 jQuery 强大的选择器引擎,使测试对现代 Web 开发人员更加友好和易读。

想了解选择元素的最佳实践? 阅读这里

但访问查询返回的 DOM 元素有所不同:

// 这样没问题,jQuery 同步返回元素
const $jqElement = $('.element')

// 这样不行!Cypress 不会同步返回元素
const $cyElement = cy.get('.element')

让我们看看为什么...

Cypress 与 jQuery 不同

问题: 当 jQuery 找不到匹配的 DOM 元素时会发生什么?

回答: 糟糕! 它返回一个空的 jQuery 集合。我们得到的是一个真实对象,但不包含想要的元素。于是我们开始手动添加条件检查和重试查询。

// $() 立即返回空集合
const $myElement = $('.element').first()

// 导致丑陋的条件检查
// 更糟的是 - 测试变得不稳定!
if ($myElement.length) {
doSomething($myElement)
}

问题: 当 Cypress 找不到匹配的 DOM 元素时会发生什么?

回答: 没关系! Cypress 会自动重试查询,直到:

1. 找到元素

cy
// cy.get() 查找 '#element',不断重试直到...
.get('#element')

// ...找到元素!
// 现在可以通过 .then 操作它
.then(($myElement) => {
doSomething($myElement)
})

2. 达到超时

cy
// cy.get() 查找 '#element-does-not-exist',不断重试直到...
// ...在超时前未找到元素
// Cypress 停止并标记测试失败
.get('#element-does-not-exist')

// ...这段代码永远不会执行...
.then(($myElement) => {
doSomething($myElement)
})

这使得 Cypress 非常健壮,能够避免其他测试工具中常见的数十种问题。考虑以下可能导致 DOM 元素查询失败的情况:

  • DOM 尚未加载完成
  • 你的框架还未完成初始化
  • XHR 请求未响应
  • 动画未完成
  • 等等...

以前,你不得不编写自定义代码来防范这些问题:混杂着随意等待、条件重试和空检查的丑陋代码。但在 Cypress 中不需要!通过内置的重试和 可配置超时, Cypress 规避了所有这些不稳定问题。

tip
核心概念

Cypress 为所有 DOM 查询包装了健壮的重试和超时逻辑,更贴近真实 Web 应用的工作方式。我们用查找 DOM 元素方式的微小改变,换来了所有测试的重大稳定性提升。彻底告别不稳定!

info

在 Cypress 中,当你想直接与 DOM 元素交互时,调用 .then() 并传入接收元素作为第一个参数的回调函数。当你想完全跳过重试和超时功能,执行传统的同步工作时,使用 Cypress.$

按文本内容查询

另一种更人性化的定位方式是按照用户能在页面上看到的内容查找。为此,可以使用方便的 cy.contains() 命令,例如:

// 查找文档中包含文本'新文章'的元素
cy.contains('新文章')

// 在 .main 内查找包含文本'新文章'的元素
cy.get('.main').contains('新文章')

这在从用户角度编写测试时很有帮助。用户只知道他们想点击标有"提交"的按钮。他们不知道它有 type 属性为 submit,或者 CSS 类为 my-submit-button

caution
国际化

如果你的应用支持多语言国际化,确保考虑使用用户可见文本来查找 DOM 元素的影响!

当元素缺失时

如前所示,Cypress 预见了 Web 应用的异步特性,不会在第一次找不到元素时就立即失败。相反,Cypress 会给你的应用一个时间窗口来完成它可能正在做的任何事情!

这被称为 timeout,大多数命令都可以通过特定的超时期限进行自定义 (默认超时为 4 秒)。 这些命令会在 API 文档中列出 timeout 选项,详细说明如何设置你希望继续尝试查找元素的毫秒数。

// 给这个元素 10 秒时间出现
cy.get('.my-slow-selector', { timeout: 10000 })

你也可以通过 配置设置: defaultCommandTimeout 全局设置超时。

tip
核心概念

为了匹配 Web 应用的行为,Cypress 是异步的,并依赖超时来知道何时停止等待应用进入预期状态。超时可以全局配置,也可以基于每个命令配置。

info
超时与性能

这里有一个性能权衡:具有较长超时期限的测试需要更长时间才能失败。命令在满足预期条件时会立即继续,因此工作测试会以应用允许的速度尽快执行。由于超时而失败的测试会消耗整个超时期限,这是设计使然。这意味着虽然你可能希望增加超时期限以适应应用的特定部分,但你不想"只是为了以防万一"而设置"超长"超时。

在本指南后面,我们将更详细地介绍 隐式断言超时

命令链

理解 Cypress 用于链接命令的机制非常重要。它代表你管理一个 Promise 链,每个命令将"主题"传递给下一个命令,直到链结束或遇到错误。开发者不需要直接使用 Promise,但理解它们的工作原理很有帮助!

与元素交互

如初始示例所示,Cypress 允许你通过使用 .click().type() 动作命令与页面上的元素交互,配合 cy.get()cy.contains() 查询命令。这是链式调用的一个很好的例子。再看一次:

cy.get('textarea.post-body').type('这是一篇优秀的文章。')

我们将 .type() 链接到 cy.get(),告诉它在 cy.get() 查询产生的主题(即 DOM 元素)上输入文本。

以下是 Cypress 提供的更多动作命令,用于与你的应用交互:

这些命令确保 某些保证, 即在执行动作前元素应处于的状态。

例如,编写 .click() 命令时,Cypress 确保元素能够被交互(就像真实用户一样)。它会自动等待直到元素达到"可操作"状态:

  • 不被隐藏
  • 不被覆盖
  • 不被禁用
  • 不在动画中

这也有助于防止在与应用交互时测试变得不稳定。通常你可以通过 force 选项覆盖此行为。

tip
核心概念

Cypress 在与元素交互时提供了简单但强大的算法。 了解更多

对元素进行断言

断言让你能够确保元素可见、具有特定属性、CSS 类或状态。断言是描述应用期望状态的命令。Cypress 会自动等待直到你的元素达到这个状态,或者如果断言未通过则测试失败。快速看一下断言的运作:

cy.get(':checkbox').should('be.disabled')

cy.get('form').should('have.class', 'form-horizontal')

cy.get('input').should('not.have.value', 'US')

在这些例子中,重要的是要注意 Cypress 会自动等待直到这些断言通过。这让你无需知道或关心元素何时最终达到这个状态。

我们将在本指南后面更多了解 断言

主题管理

新的 Cypress 链总是以 cy.[command] 开始,其中 command 产生的内容决定了接下来可以调用(链式调用)哪些其他命令。

所有命令都会产生一个值。

每个命令指定它产生什么值。例如,

  • cy.clearCookies() 产生 null。只要下一个命令不期望接收主题,你就可以链接产生 null 的命令。
  • cy.contains() 产生一个 DOM 元素,允许进一步链接命令(假设它们期望 DOM 主题)如 .click() 或甚至 cy.contains() 再次。
  • .click() 产生与最初给定的相同主题。

一些命令需要前一个主题。

  • .click() 需要前一个命令产生的 DOM 元素。
  • .its() 需要主题,但可以是任何类型。
  • cy.contains() 的行为取决于前一个主题。如果直接链接到 cy,或者前一个命令产生 null,它会查找整个文档。但如果主题是 DOM 元素,它只会在该容器内查找。
  • cy.clearCookies() 不需要前一个主题 - 它可以链接到任何命令,甚至 .end()

示例:

这实际上比听起来更直观。

cy.clearCookies() // 产生 null
.visit('/fixtures/dom.html') // 不关心前一个主题

cy.get('.main-container') // 产生匹配的 DOM 元素数组
.contains('头条新闻') // 产生包含内容的第一个 DOM 元素
.click() // 产生与前一个命令相同的 DOM 元素
tip
核心概念

Cypress 命令不会返回它们的主题,而是产生它们。记住:Cypress 命令是异步的,会在稍后排队执行。在执行期间,主题从一个命令传递到下一个命令,Cypress 在命令之间运行大量有用的代码以确保一切有序。

info
在操作 DOM 后不要继续链式调用

虽然在 Cypress 中可以在操作 DOM 后继续链式调用,但这通常不安全,可能导致元素过时。更多细节请参阅 重试能力指南

但经验法则是简单的:如果你执行了一个动作,如导航页面、点击按钮或滚动视口,在那里结束命令链,并从 cy 重新开始。

info

为了解决引用元素的需求,Cypress 有一个功能 称为别名。别名帮助你存储和保存引用以供将来使用。

使用 .then() 操作主题

想直接进入命令流并获取主题吗?没问题,在命令链中添加一个 .then()。当前一个命令解析时,它会调用你的回调函数,并将产生的主题作为第一个参数。

如果你希望在 .then() 后继续链式调用命令,你需要指定你想传递给这些命令的主题,这可以通过返回非 nullundefined 的值来实现。Cypress 会将该值传递给下一个命令。

看个例子:

cy
// 查找 id 为 'some-link' 的元素
.get('#some-link')

.then(($myElement) => {
// ...用一些任意代码处理主题

// 获取它的 href 属性
const href = $myElement.prop('href')

// 去除 '#' 字符及其后的所有内容
return href.replace(/(#.*)/, '')
})
.then((href) => {
// href 现在是新主题
// 我们现在可以处理它
})
tip
核心概念

我们在核心概念指南中有更多关于cy.then()的例子和用例,教你如何正确处理异步代码,何时使用变量,以及什么是别名。

使用别名引用之前的主题

Cypress 有一些额外的功能可以快速引用之前的主题,称为别名。它看起来像这样:

cy.get('.my-selector')
.as('myElement') // 设置别名
.click()

/* 更多操作 */

cy.get('@myElement') // 像之前一样重新查询 DOM
.click()

这让我们可以重用查询使测试更易读,并且它会自动为我们处理 DOM 更新时的重新查询。这在处理进行大量重新渲染的前端框架时特别有用!

命令是异步的

非常重要的一点是理解 Cypress 命令在被调用时不会立即执行任何操作,而是将自己排队等待稍后运行。这就是我们说 Cypress 命令是异步的意思。

看这个简短的测试:

it('点击时隐藏元素', () => {
cy.visit('/my/resource/path') // 目前什么都没发生

cy.get(".hides-when-clicked") // 仍然什么都没发生
.should("be.visible") // 绝对什么都没发生
.click() // 不,还是没发生

cy.get('.hides-when-clicked') // 仍然什么都没发生
.should('not.be.visible') // 肯定什么都没发生
})

// 好的,测试函数执行完毕...
// 我们已经排队了所有这些命令
// 现在 Cypress 将开始按顺序运行它们!

Cypress 直到测试函数退出才会启动浏览器自动化。

混合异步和同步代码

记住 Cypress 命令是异步运行的,这一点很重要,如果你尝试将 Cypress 命令与同步代码混合使用。同步代码会立即执行 - 不会等待上面的 Cypress 命令执行。

错误用法

在下面的例子中,el 会立即求值,在 cy.visit() 执行之前,所以总是会得到一个空数组。

it('不符合我们的预期', () => {
cy.visit('/my/resource/path') // 目前什么都没发生

cy.get('.awesome-selector') // 仍然什么都没发生
.click() // 不,还是没发生

// Cypress.$ 是同步的,所以会立即求值
// 还没有元素可找,因为
// cy.visit() 只是排队等待访问
// 并没有实际访问应用
let el = Cypress.$('.new-el') // 立即求值为 []

if (el.length) {
// 立即求值为 0
cy.get('.another-selector')
} else {
// 这总是会运行
// 因为 'el.length' 是 0
// 当代码执行时
cy.get('.optional-selector')
}
})

// 好的,测试函数执行完毕...
// 我们已经排队了所有这些命令
// 现在 Cypress 将开始按顺序运行它们!

正确用法

以下是上述代码可以重写的一种方式,以确保命令按预期运行。

it('不符合我们的预期', () => {
cy.visit('/my/resource/path') // 目前什么都没发生

cy.get('.awesome-selector') // 仍然什么都没发生
.click() // 不,还是没发生
.then(() => {
// 将这段代码放在 .then() 内确保
// 它在 Cypress 命令"执行"后运行
let el = Cypress.$('.new-el') // 在 .then() 后求值

if (el.length) {
cy.get('.another-selector')
} else {
cy.get('.optional-selector')
}
})
})

// 好的,测试函数执行完毕...
// 我们已经排队了所有这些命令
// 现在 Cypress 将开始按顺序运行它们!

错误用法

在下面的例子中,对 username 值的检查会立即求值,在 cy.visit() 执行之前,所以总是会得到 undefined

it('测试', () => {
let username = undefined // 立即求值为 undefined

cy.visit('https://example.cypress.io') // 目前什么都没发生
cy.get('.user-name') // 仍然什么都没发生
.then(($el) => {
// 目前什么都没发生
// 这行代码在 .then 执行后求值
username = $el.text()
})

// 这在上面的 .then() 之前求值
// 所以 username 仍然是 undefined
if (username) {
// 立即求值为 undefined
cy.contains(username).click()
} else {
// 这总是会运行
// 因为 username 总是
// 求值为 undefined
cy.contains('我的个人资料').click()
}
})

// 好的,测试函数执行完毕...
// 我们已经排队了所有这些命令
// 现在 Cypress 将开始按顺序运行它们!

正确用法

以下是上述代码可以重写的一种方式,以确保命令按预期运行。

it('测试', () => {
let username = undefined // 立即求值为 undefined

cy.visit('https://example.cypress.io') // 目前什么都没发生
cy.get('.user-name') // 仍然什么都没发生
.then(($el) => {
// 目前什么都没发生
// 这行代码在 .then() 执行后求值
username = $el.text()

// 在 .then() 执行后求值
// 这是从 $el.text() 获取的正确值
if (username) {
cy.contains(username).click()
} else {
cy.get('我的个人资料').click()
}
})
})

// 好的,测试函数执行完毕...
// 我们已经排队了所有这些命令
// 现在 Cypress 将开始按顺序运行它们!
tip
核心概念

每个 Cypress 命令(和命令链)都会立即返回,只是被附加到一个队列中等待稍后执行。

你故意无法对命令的返回值做任何有用的事情。命令在幕后排队并完全管理。

我们这样设计 API 是因为 DOM 是一个高度可变的对象,经常会过时。为了让 Cypress 防止不稳定,并知道何时继续,我们以高度受控的确定性方式管理命令。

info
为什么不能使用 async / await?

如果你是现代 JS 程序员,听到"异步"可能会想:为什么不能直接用 async/await 而需要学习专有 API?

Cypress 的 API 设计与你可能习惯的方式非常不同:但这些设计模式是非常有意的。我们将在本指南后面更详细地介绍。

避免循环

使用 JavaScript 循环命令如 while 可能会有意想不到的效果。假设我们的应用在加载时显示一个随机数。

手动刷新浏览器页面直到数字7出现

我们希望测试在找到数字7时停止。如果显示任何其他数字,测试重新加载页面并再次检查。

注意: 你可以在我们的示例库中找到这个应用和正确的测试。

错误测试

下面编写的测试不会工作,很可能会使你的浏览器崩溃。

let found7 = false

while (!found7) {
// 这会安排无限数量的
// "cy.get..." 命令,最终在
// 任何命令有机会运行之前崩溃
// 并将 found7 设置为 true
cy.get('#result')
.should('not.be.empty')
.invoke('text')
.then(parseInt)
.then((number) => {
if (number === 7) {
found7 = true
cy.log('幸运数字 **7**')
} else {
cy.reload()
}
})
}

上面的测试不断向测试链添加更多的 cy.get('#result') 命令而不执行任何命令!命令链不断增长,但从不执行 - 因为测试函数永远不会完成运行。while 循环不允许 Cypress 开始执行甚至第一个 cy.get(...) 命令。

正确测试

我们需要给测试一个机会在决定是否需要继续之前运行一些命令。因此正确的测试会使用递归。

const checkAndReload = () => {
// 获取元素的文本,转换为数字
cy.get('#result')
.should('not.be.empty')
.invoke('text')
.then(parseInt)
.then((number) => {
// 如果找到预期的数字
// 停止添加更多命令
if (number === 7) {
cy.log('幸运数字 **7**')

return
}

// 否则通过重新加载后调用函数
// 插入更多 Cypress 命令
cy.wait(500, { log: false })
cy.reload()
checkAndReload()
})
}

cy.visit('public/index.html')
checkAndReload()

测试运行并正确完成。

测试重新加载页面直到数字7出现

你可以通过这个短视频了解这个例子:

https://www.youtube.com/watch?v=5Z8BaPNDfvA

命令串行运行

测试函数运行完毕后,Cypress 开始执行使用 cy.* 命令链排队的命令。

再看一个例子

it('点击时隐藏元素', () => {
cy.visit('/my/resource/path') // 1.

cy.get('.hides-when-clicked') // 2
.should('be.visible') // 3
.click() // 4

cy.get('.hides-when-clicked') // 5
.should('not.be.visible') // 6
});

上面的测试会按以下顺序执行:

  1. 访问 URL(或挂载组件)
  2. 通过选择器查找元素
  3. 断言元素可见
  4. 对该元素执行点击动作
  5. 通过选择器查找元素
  6. 断言元素不再可见

这些动作总是串行(一个接一个)发生,永远不会并行(同时)发生。为什么?

为了说明这一点,让我们重新审视那组动作,并揭示 Cypress 在每个步骤中为我们做的一些隐藏的 ✨魔法✨

  1. 访问 URL ✨ 并等待页面加载事件在所有外部资源加载完成后触发 ✨(或挂载组件 ✨ 并等待组件完成挂载 ✨)
  2. 通过选择器查找元素 ✨ 并重试直到在 DOM 中找到它
  3. 断言元素可见 ✨ 并重试直到断言通过
  4. 对该元素执行点击动作 ✨ 在我们等待元素达到可操作状态后
  5. 通过选择器查找元素 ✨ 并重试直到在 DOM 中找到它
  6. 断言元素不再可见 ✨ 并重试直到断言通过

如你所见,Cypress 做了很多额外工作来确保应用状态与我们的命令期望匹配。每个命令可能很快解析(快到你看不到它们处于挂起状态),但其他命令可能需要几秒甚至几十秒才能解析。

虽然大多数命令在几秒后超时,但其他专门命令如 cy.visit() 会自然等待更长时间才超时。

这些命令在 Cypress 配置 中有自己的特定超时值。

tip
核心概念

任何必要的等待或重试都必须在一个步骤成功完成后才开始下一步。如果它们在超时前没有成功完成,测试将失败。

Cypress 命令队列

虽然 API 看起来类似于 Promise,带有 then() 语法,但 Cypress 命令和查询不是 Promise - 它们是传递给中央队列的串行命令,在稍后异步执行。这些命令旨在提供确定性、可重复和一致的测试。

几乎所有命令都带有内置的 重试能力。没有 重试能力,断言会随机失败。这将导致不稳定、不一致的结果。

info

虽然 Cypress 确实有一个 .then() 命令,但 Cypress 命令不是 Promise,不能被 await。如果你想了解更多关于处理异步 Cypress 命令的信息,请阅读我们的 变量和别名指南

命令还有一些设计选择,习惯于基于 Promise 测试的开发人员可能会觉得意外。它们是 Cypress 的故意决定,而不是技术限制。

  1. 你不能竞争或同时运行多个命令(并行)。
  2. 你不能为失败的命令添加 .catch 错误处理程序。

Cypress 的全部目的(以及它与其他测试工具非常不同的地方)是创建一致、不稳定的测试,每次运行表现相同。实现这一点并非没有代价 - 我们做出了一些权衡,最初可能对习惯于使用 Promise 或其他库的开发人员不熟悉。

让我们深入了解每个权衡:

你不能竞争或同时运行多个命令

Cypress 保证它会以确定性和相同的方式执行所有命令和查询每次运行。

许多 Cypress 命令以某种方式改变浏览器状态。

上述命令都不是幂等的;它们都会产生副作用。竞争命令是不可能的,因为命令必须以受控的串行方式运行以创建一致性。因为集成和端到端测试主要模拟真实用户的操作,Cypress 将其命令执行模型设计为真实用户逐步工作的方式。

你不能为失败的命令添加 .catch 错误处理程序

在 Cypress 中,没有内置的错误恢复机制来处理失败的命令。命令最终会通过,或者如果失败,所有剩余命令不会执行,整个测试失败。

你可能会想:

如何创建条件控制流,使用 if/else?这样如果元素存在(或不存在),我可以选择做什么?

Cypress 不支持这种条件控制流,因为它会导致非确定性测试 - 不同运行可能表现不同,这使得它们不太一致,也不太适合验证你的应用的正确性。一般来说,只有少数非常特定的情况下,你可以在 Cypress 中使用命令创建控制流。

尽管如此,只要你意识到控制流的潜在陷阱,在 Cypress 中这样做是可能的!你可以在这里阅读所有关于如何进行条件测试的内容。

断言

正如我们之前在本指南中提到的:

note

断言描述你的元素、对象和应用的期望状态。

Cypress 与其他测试工具的不同之处在于断言会自动重试。将它们视为守卫 - 断言描述你的应用应该是什么样子,Cypress 会自动阻塞、等待和重试直到达到该状态。

用英语表达断言

让我们看看如何用英语描述断言:

note

点击这个 <button> 后,我期望它的类为 active

要在 Cypress 中表达这一点,你会写:

cy.get('button').click()
cy.get('button').should('have.class', 'active')

即使 .active 类是异步应用到按钮的,经过不确定的时间,甚至如果按钮暂时从 DOM 中完全移除(例如被等待旋转图标替换),上面的测试也会通过。

// 即使我们在两秒后添加类...
// 这个测试仍然会通过!
$('button').on('click', (e) => {
setTimeout(() => {
$(e.target).addClass('active')
}, 2000)
})

这是另一个例子。

note

向我的服务器发出 HTTP 请求后,我期望响应体等于 {name: 'Jane'}

要用断言表达这一点,你会写:

cy.request('/users/1').its('body').should('deep.eq', { name: 'Jane' })

何时断言?

尽管 Cypress 提供了数十种断言,有时最好的测试可能根本不包含任何断言!这怎么可能?断言不是测试的基本部分吗?

考虑这个例子:

cy.visit('/home')

cy.get('.main-menu').contains('新项目').click()

cy.get('.title').type('我的超棒项目')

cy.get('form').submit()

没有任何显式断言,这个测试有多种失败方式。以下是几种:

  • 初始的 cy.mount()cy.visit() 可能返回非成功的响应。
  • 任何 cy.get() 查询可能无法在 DOM 中找到它们的元素。
  • 我们想要 .click() 的元素可能被另一个元素覆盖。
  • 我们想要 .type() 的输入可能被禁用。
  • 表单提交可能导致非成功状态码。
  • 页面内 JS(被测试的应用)或组件可能抛出错误。
tip
核心概念

使用 Cypress,你不必编写显式断言也能拥有有用的测试。没有任何 expect().should(),几行 Cypress 代码就能确保客户端和服务端的数千行代码正常工作。

这是因为许多命令都有内置的隐式断言,为你提供高级别的信心,确保你的应用按预期工作。

隐式断言

许多命令有默认的内置断言,或者说有要求,可能导致它们失败而不需要你添加显式断言。

例如:

  • cy.visit() 期望页面发送 text/html 内容和 200 状态码。
  • cy.request() 期望远程服务器存在并提供响应。
  • cy.contains() 期望包含内容的元素最终存在于 DOM 中。
  • cy.get() 期望元素最终存在于 DOM 中。
  • .find() 也期望元素最终存在于 DOM 中。
  • .type() 期望元素最终处于可输入状态。
  • .click() 期望元素最终处于可操作状态。
  • .its() 期望最终在当前主题上找到属性。

某些命令可能有特定要求导致它们立即失败而不重试,如 cy.request()

其他命令,如 DOM 查询会自动 重试 并等待它们对应的元素存在后才失败。

动作命令会自动等待它们的元素达到 可操作状态 后才失败。

tip
核心概念

所有 DOM 命令自动等待它们的元素存在于 DOM 中。

你永远不需要在查询 DOM 后写 .should('exist')

大多数命令给你灵活性来覆盖或绕过它们可能失败的默认方式,通常通过传递 {force: true} 选项。

示例 #1: 存在性和可操作性

cy
// 有一个隐式断言,即这个
// 按钮必须在 DOM 中存在才能继续
.get('button')

// 在发出点击之前,这个按钮必须是"可操作的"
// 它不能被禁用、被覆盖或隐藏在视图中
.click()

Cypress 会自动等待元素通过它们的隐式断言。更多关于如何确定超时的信息,请参见下面的 超时

示例 #2: 反转隐式断言

大多数情况下,查询元素时你期望它们最终存在。但有时你希望等到它们不存在。

你只需要添加该断言,Cypress 就会跳过隐式等待元素存在。

cy.get('button.close').click()

// 现在 Cypress 会等待直到这个
// <button> 不在 DOM 中
cy.get('button.close').should('not.exist')

// 现在确保这个 #modal 不在 DOM 中
// 并自动等待直到它消失!
cy.get('#modal').should('not.exist')
tip
核心概念

如果你想禁用默认的存在断言,可以向任何 DOM 命令添加 .should('not.exist')

示例 #3: 其他隐式断言

其他命令有其他不涉及 DOM 的隐式断言。

例如,.its() 要求你询问的属性存在于对象上。

// 创建一个空对象
const obj = {}

// 1 秒后设置 'foo' 属性
setTimeout(() => {
obj.foo = 'bar'
}, 1000)

// .its() 会等待直到 'foo' 属性出现在对象上
cy.wrap(obj).its('foo')

断言列表

Cypress 捆绑了 ChaiChai-jQuerySinon-Chai 来提供 内置断言。你可以在断言列表参考中看到它们的全面列表。你也可以 将自己的断言编写为 Chai 插件并在 Cypress 中使用它们。

编写断言

在 Cypress 中有两种编写断言的方式:

  1. 作为 Cypress 命令: 使用 .should().and()
  2. 作为 Mocha 断言: 使用 expect

命令断言

使用 .should().and() 命令是在 Cypress 中进行断言的首选方式。这些是典型的 Cypress 命令,意味着它们应用于命令链中当前产生的主题。

// 这里的主题是第一个 <tr>
// 这断言 <tr> 有 .active 类
cy.get('tbody tr:first').should('have.class', 'active')

你可以使用 .and() 链接多个断言,这是 .should() 的别名,使内容更易读:

cy.get('#header a')
.should('have.class', 'active')
.and('have.attr', 'href', '/users')

因为 .should('have.class') 不会改变主题,.and('have.attr') 是针对同一个元素执行的。当你需要快速对单个主题进行多个断言时,这很方便。

Mocha 断言

使用 expect 允许你对任何 JavaScript 对象进行断言,而不仅仅是当前主题。这可能是你在单元测试中看到断言编写的方式:

// 这里的显式主题是布尔值:true
expect(true).to.be.true
info
你知道可以在 Cypress 中编写单元测试吗?

查看我们的示例库中的单元测试React 组件单元测试

Mocha 断言在以下情况下非常有用:

  • 在进行断言之前执行自定义逻辑。
  • 对同一主题进行多个断言。

.should() 断言允许我们传递一个回调函数,该函数将产生的主题作为其第一个参数。这类似于 .then(),不同之处在于 Cypress 会自动等待和重试回调函数中的所有内容直到通过。

info
复杂断言

下面的例子是一个用例,我们对多个元素进行断言。使用 .should() 回调函数是从父元素查询多个子元素并断言它们状态的好方法。

这样做使你能够通过确保后代状态符合你的期望来阻塞和保护 Cypress,而无需使用常规的 Cypress DOM 命令单独查询它们。

cy.get('p').should(($p) => {
// 将我们的主题从 DOM 元素转换
// 为所有 p 标签的文本数组
let texts = $p.map((i, el) => {
return Cypress.$(el).text()
})

// jQuery map 返回 jQuery 对象
// .get() 将其转换为数组
texts = texts.get()

// 数组长度应为 3
expect(texts).to.have.length(3)

// 包含这些特定内容
expect(texts).to.deep.eq([
'第一个 p 标签的文本',
'第二个 p 标签的更多文本',
'第三个 p 标签的更多文本',
])
})
danger

确保 .should() 安全

当使用带有 .should() 的回调函数时,确保整个函数可以多次执行而没有副作用。Cypress 对其应用重试逻辑:如果有失败,它会重复运行断言直到达到超时。这意味着你的代码应该是可重试的。技术术语是你的代码必须是幂等的。

超时

几乎所有命令都可能以某种方式超时。

所有断言,无论是默认的还是你添加的,都共享相同的超时值。

应用超时

你可以修改命令的超时。这个超时会影响它的默认断言(如果有)和你添加的任何特定断言。

记住因为断言用于描述前一个命令的条件 - timeout 修改在前一个命令上,而不是断言上。

示例 #1: 隐式断言

// 因为 .get() 有一个隐式断言
// 这个元素必须存在,它可能会超时并失败
cy.get('.mobile-nav')

在底层 Cypress:

  • 查询元素 .mobile-nav

    并等待最多 4 秒让它存在于 DOM 中

示例 #2: 额外断言

// 我们向测试添加了 2 个断言
cy.get('.mobile-nav').should('be.visible').and('contain', '首页')

在底层 Cypress:

  • 查询元素 .mobile-nav

    并等待最多 4 秒让它存在于 DOM 中✨ ✨并可见✨ ✨并包含文本:首页

Cypress 将等待所有断言通过的总时间是 cy.get() timeout 的持续时间(即 4 秒)。

超时可以基于每个命令修改,这将影响所有隐式断言和链接到该命令后的任何断言。

示例 #3: 修改超时

// 我们修改了超时,这会影响隐式
// 断言以及所有显式断言
cy.get('.mobile-nav', { timeout: 10000 })
.should('be.visible')
.and('contain', '首页')

在底层 Cypress:

  • 获取元素 .mobile-nav

    并等待最多 10 秒让它存在于 DOM 中✨ ✨并可见✨ ✨并包含文本:首页

注意这个超时已经流向所有断言,Cypress 现在将总共等待最多 10 秒让它们全部通过。

danger

注意你永远不会在断言内部更改超时。timeout 参数总是在命令内部。

// 🚨 无效
cy.get('.selector').should('be.visible', { timeout: 1000 })
// ✅ 正确方式
cy.get('.selector', { timeout: 1000 }).should('be.visible')

记住,你是在重试带有附加断言的命令,而不仅仅是断言!

默认值

Cypress 根据命令类型提供几种不同的超时值。

我们根据预期某些动作需要的时间设置了它们的默认超时持续时间。

例如:

  • cy.visit() 加载远程页面,直到所有外部资源完成加载阶段才会解析。这可能需要一段时间,所以它的默认超时设置为 60000ms
  • cy.exec() 运行系统命令,如种子数据库。我们预期这可能需要很长时间,它的默认超时设置为 60000ms
  • cy.wait() 实际上使用 2 种不同的超时。当等待 路由别名时,我们等待匹配请求 5000ms,然后额外等待服务器响应 30000ms。我们预期你的应用会快速发出匹配请求,但我们预期服务器响应可能需要更长时间。

这使得大多数其他命令包括所有 DOM 查询默认在 4000ms 后超时。