Cypress App 简介
这是理解如何使用 Cypress 进行测试的最重要指南。请仔细阅读并理解。如有疑问请提出,以便我们改进。
你将学到
- 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", "我的第一篇文章");
});
});
describe('文章资源', () => {
it('创建新文章', () => {
cy.mount(<PostBuilder />) // 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", "我的第一篇文章");
});
});
你能读懂这段代码吗?它大致是这样的:
- 访问
/posts/new
页面(或挂载PostBuilder
组件) - 查找类名为
post-title
的<input>
- 输入"我的第一篇文章"
- 查找类名为
post-body
的<input>
- 输入"你好,世界!"
- 查找包含文本
提交
的元素 - 点击它
- 查找
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()
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 规避了所有这些不稳定问题。
Cypress 为所有 DOM 查询包装了健壮的重试和超时逻辑,更贴近真实 Web 应用的工作方式。我们用查找 DOM 元素方式的微小改变,换来了所有测试的重大稳定性提升。彻底告别不稳定!
按文本内容查询
另一种更人性化的定位方式是按照用户能在页面上看到的内容查找。为此,可以使用方便的
cy.contains()
命令,例如:
// 查找文档中包含文本'新文章'的元素
cy.contains('新文章')
// 在 .main 内查找包含文本'新文章'的元素
cy.get('.main').contains('新文章')
这在从用户角度编写测试时很有帮助。用户只知道他们想点击标有"提交"的按钮。他们不知道它有 type
属性为 submit
,或者 CSS 类为 my-submit-button
。
如果你的应用支持多语言国际化,确保考虑使用用户可见文本来查找 DOM 元素的影响!
当元素缺失时
如前所示,Cypress 预见了 Web 应用的异步特性,不会在第一次找不到元素时就立即失败。相反,Cypress 会给你的应用一个时间窗口来完成它可能正在做的任何事情!
这被称为 timeout
,大多数命令都可以通过特定的超时期限进行自定义
(默认超时为 4 秒)。
这些命令会在 API 文档中列出 timeout
选项,详细说明如何设置你希望继续尝试查找元素的毫秒数。
// 给这个元素 10 秒时间出现
cy.get('.my-slow-selector', { timeout: 10000 })
你也可以通过
配置设置: defaultCommandTimeout
全局设置超时。
为了匹配 Web 应用的行为,Cypress 是异步的,并依赖超时来知道何时停止等待应用进入预期状态。超时可以全局配置,也可以基于每个命令配置。
这里有一个性能权衡:具有较长超时期限的测试需要更长时间才能失败。命令在满足预期条件时会立即继续,因此工作测试会以应用允许的速度尽快执行。由于超时而失败的测试会消耗整个超时期限,这是设计使然。这意味着虽然你可能希望增加超时期限以适应应用的特定部分,但你不想"只是为了以防万一"而设置"超长"超时。
命令链
理解 Cypress 用于链接命令的机制非常重要。它代表你管理一个 Promise 链,每个命令将"主题"传递给下一个命令,直到链结束或遇到错误。开发者不需要直接使用 Promise,但理解它们的工作原理很有帮助!
与元素交互
如初始示例所示,Cypress 允许你通过使用 .click()
和
.type()
动作命令与页面上的元素交互,配合
cy.get()
或 cy.contains()
查询命令。这是链式调用的一个很好的例子。再看一次:
cy.get('textarea.post-body').type('这是一篇优秀的文章。')
我们将 .type()
链接到
cy.get()
,告诉它在 cy.get()
查询产生的主题(即 DOM 元素)上输入文本。
以下是 Cypress 提供的更多动作命令,用于与你的应用交互:
.blur()
- 使聚焦的 DOM 元素失焦.focus()
- 聚焦到 DOM 元素.clear()
- 清除输入框或文本区域的值.check()
- 勾选复选框或单选按钮.uncheck()
- 取消勾选复选框.select()
- 选择<select>
中的<option>
.dblclick()
- 双击 DOM 元素.rightclick()
- 右键点击 DOM 元素
这些命令确保 某些保证, 即在执行动作前元素应处于的状态。
例如,编写 .click()
命令时,Cypress
确保元素能够被交互(就像真实用户一样)。它会自动等待直到元素达到"可操作"状态:
- 不被隐藏
- 不被覆盖
- 不被禁用
- 不在动画中
这也有助于防止在与应用交互时测试变得不稳定。通常你可以通过 force
选项覆盖此行为。
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 元素
Cypress 命令不会返回它们的主题,而是产生它们。记住:Cypress 命令是异步的,会在稍后排队执行。在执行期间,主题从一个命令传递到下一个命令,Cypress 在命令之间运行大量有用的代码以确保一切有序。
虽然在 Cypress 中可以在操作 DOM 后继续链式调用,但这通常不安全,可能导致元素过时。更多细节请参阅 重试能力指南。
但经验法则是简单的:如果你执行了一个动作,如导航页面、点击按钮或滚动视口,在那里结束命令链,并从 cy
重新开始。
为了解决引用元素的需求,Cypress 有一个功能 称为别名。别名帮助你存储和保存引用以供将来使用。
使用 .then()
操作主题
想直接进入命令流并获取主题吗?没问题,在命令链中添加一个 .then()。当前一个命令解析时,它会调用你的回调函数,并将产生的主题作为第一个参数。
如果你希望在 .then()
后继续链式调用命令,你需要指定你想传递给这些命令的主题,这可以通过返回非 null
或 undefined
的值来实现。Cypress 会将该值传递给下一个命令。
看个例子:
cy
// 查找 id 为 'some-link' 的元素
.get('#some-link')
.then(($myElement) => {
// ...用一些任意代码处理主题
// 获取它的 href 属性
const href = $myElement.prop('href')
// 去除 '#' 字符及其后的所有内容
return href.replace(/(#.*)/, '')
})
.then((href) => {
// href 现在是新主题
// 我们现在可以处理它
})
使用别名引用之前的主题
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 将开始按顺序运行它们!
it('点击时隐藏元素', () => {
cy.mount(<MyComponent />) // 目前什么都没发生
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 将开始按顺序运行它们!
每个 Cypress 命令(和命令链)都会立即返回,只是被附加到一个队列中等待稍后执行。
你故意无法对命令的返回值做任何有用的事情。命令在幕后排队并完全管理。
我们这样设计 API 是因为 DOM 是一个高度可变的对象,经常会过时。为了让 Cypress 防止不稳定,并知道何时继续,我们以高度受控的确定性方式管理命令。
如果你是现代 JS 程序员,听到"异步"可能会想:为什么不能直接用 async/await
而需要学习专有 API?
Cypress 的 API 设计与你可能习惯的方式非常不同:但这些设计模式是非常有意的。我们将在本指南后面更详细地介绍。
避免循环
使用 JavaScript 循环命令如 while
可能会有意想不到的效果。假设我们的应用在加载时显示一个随机数。

我们希望测试在找到数字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()
测试运行并正确完成。

你可以通过这 个短视频了解这个例子:
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
});
it('点击时隐藏元素', () => {
cy.mount(<MyComponent />) // 1.
cy.get('.hides-when-clicked') // 2
.should('be.visible') // 3
.click() // 4
cy.get('.hides-when-clicked') // 5
.should('not.be.visible') // 6
});
上面的测试会按以下顺序执行:
- 访问 URL(或挂载组件)
- 通过选择器查找元素
- 断言元素可见
- 对该元素执行点击动作
- 通过选择器查找元素
- 断言元素不再可见
这些动作总是串行(一个接一个)发生,永远不会并行(同时)发生。为什么?
为了说明这一点,让我们重新审视那组动作,并揭示 Cypress 在每个步骤中为我们做的一些隐藏的 ✨魔法✨:
- 访问 URL ✨ 并等待页面加载事件在所有外部资源加载完成后触发 ✨(或挂载组件 ✨ 并等待组件完成挂载 ✨)
- 通过选择器查找元素 ✨ 并重试直到在 DOM 中找到它 ✨
- 断言元素可见 ✨ 并重试直到断言通过 ✨
- 对该元素执行点击动作 ✨ 在我们等待元素达到可操作状态后 ✨
- 通过选择器查找元素 ✨ 并重试直到在 DOM 中找到它 ✨
- 断言元素不再可见 ✨ 并重试直到断言通过 ✨
如你所见,Cypress 做了很多额外工作来确保应用状态与我们的命令期望匹配。每个 命令可能很快解析(快到你看不到它们处于挂起状态),但其他命令可能需要几秒甚至几十秒才能解析。
虽然大多数命令在几秒后超时,但其他专门命令如
cy.visit()
会自然等待更长时间才超时。
这些命令在 Cypress 配置 中有自己的特定超时值。
任何必要的等待或重试都必须在一个步骤成功完成后才开始下一步。如果它们在超时前没有成功完成,测试将失败。
Cypress 命令队列
虽然 API 看起来类似于 Promise,带有 then()
语法,但 Cypress 命令和查询不是 Promise - 它们是传递给中央队列的串行命令,在稍后异步执行。这些命令旨在提供确定性、可重复和一致的测试。
几乎所有命令都带有内置的 重试能力。没有 重试能力,断言会随机失败。这将导致不稳定、不一致的结果。
命令还有一些设计选择,习惯于基于 Promise 测试的开发人员可能会觉得意外。它们是 Cypress 的故意决定,而不是技术限制。
- 你不能竞争或同时运行多个命令(并行)。
- 你不能为失败的命令添加
.catch
错误处理程序。
Cypress 的全部目的(以及它与其他测试工具非常不同的地方)是创建一致、不稳定的测试,每次运行表现相同。实现这一点并非没有代价 - 我们做出了一些权衡,最初可能对习惯于使用 Promise 或其他库的开发人员不熟悉。
让我们深入了解每个权衡:
你不能竞争或同时运行多个命令
Cypress 保证它会以确定性和相同的方式执行所有命令和查询每次运行。
许多 Cypress 命令以某种方式改变浏览器状态。
cy.request()
自动从远程服务器获取和设置 cookie。cy.clearCookies()
清除所有浏览器 cookie。.click()
导致你的应用响应点击事件。
上述命令都不是幂等的;它们都会产生副作用。竞争命令是不可能的,因为命令必须以受控的串行方式运行以创建一致性。因为集成和端到端测试主要模拟真实用户的操作,Cypress 将其命令执行模型设计为真实用户逐步工作的方式。
你不能为失败的命令添加 .catch
错误处理程序
在 Cypress 中,没有内置的错误恢复机制来处理失败的命令。命令最终会通过,或者如果失败,所有剩余命令不会执行,整个测试失败。
你可能会想:
如何创建条件控制流,使用 if/else?这样如果元素存在(或不存在),我可以选择做什么?
Cypress 不支持这种条件控制流,因为它会导致非确定性测试 - 不同运行可能表现不同,这使得它们不太一致,也不太适合验证你的应用的正确性。一般来说,只有少数非常特定的情况下,你可以在 Cypress 中使用命令创建控制流。
尽管如此,只要你意识到控制流的潜在陷阱,在 Cypress 中这样做是可能的!你可以在这里阅读所有关于如何进行条件测试的内容。
断言
正如我们之前在本指南中提到的:
断言描述你的元素、对象和应用的期望状态。
Cypress 与其他测试工具的不同之处在于断言会自动重试。将它们视为守卫 - 断言描述你的应用应该是什么样子,Cypress 会自动阻塞、等待和重试直到达到该状态。
用英语表达断言
让我们看看如何用英语描述断言:
点击这个 <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)
})
这是另一个例子。
向我的服务器发出 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(<MyComponent />)
cy.get('.main-menu').contains('新项目').click()
cy.get('.title').type('我的超棒项目')
cy.get('form').submit()
没有任何显式断言,这个测试有多种失败方式。以下是几种:
- 初始的
cy.mount()
或cy.visit()
可能返回非成功的响应。 - 任何
cy.get()
查询可能无法在 DOM 中找到它们的元素。 - 我们想要
.click()
的元素可能被另一个元素覆盖。 - 我们想要
.type()
的输入可能被禁用。 - 表单提交可能导致非成功状态码。
- 页面内 JS(被测试的应用)或组件可能抛出错误。
使用 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 查询会自动 重试 并等待它们对应的元素存在后才失败。
动作命令会自动等待它们的元素达到 可操作状态 后才失败。
所有 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')
如果你想禁用默认的存在断言,可以向任何 DOM 命令添加
.should('not.exist')
。
示例 #3: 其他隐式断言
其他命令有其他不涉及 DOM 的隐式断言。
例如,.its()
要求你询问的属性存在于对象上。
// 创建一个空对象
const obj = {}
// 1 秒后设置 'foo' 属性
setTimeout(() => {
obj.foo = 'bar'
}, 1000)
// .its() 会等待直到 'foo' 属性出现在对象上
cy.wrap(obj).its('foo')
断言列表
Cypress 捆绑了 Chai、 Chai-jQuery 和 Sinon-Chai 来提供 内置断言。你可以在断言列表参考中看到它们的全面列表。你也可以 将自己的断言编写为 Chai 插件并在 Cypress 中使用它们。
编写断言
在 Cypress 中有两种编写断言的方式:
命令断言
使用 .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
查看我们的示例库中的单元测试和 React 组件单元测试。
Mocha 断言在以下情况下非常有用:
- 在进行断言之前执行自定义逻辑。
- 对同一主题进行多个断言。
.should()
断言允许我们传递一个回调函数,该函数将产生的主题作为其第一个参数。这类似于
.then()
,不同之处在于 Cypress 会自动等待和重试回调函数中的所有内容直到通过。
下面的例子是一个用例,我们对多个元素进行断言。使用 .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 标签的更多文本',
])
})
超时
几乎所有命令都可能以某种方式超时。
所有断言,无论是默认的还是你添加的,都共享相同的超时值。
应用超时
你可以修改命令的超时。这个超时会影响它的默认断言(如果有)和你添加的任何特定断言。
记住因为断言用于描述前一个命令的条件 - 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 秒让它们全部通过。
注意你永远不会在断言内部更改超时。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 后超时。