网络请求
你将学习到
- 在Cypress中测试网络请求的策略
- 如何模拟和等待网络响应
- 测试GraphQL查询和变更的最佳实践
注意: 如果你需要发起HTTP请求,请查看 cy.request()
测试策略
Cypress帮助你测试应用程序中HTTP请求的整个生命周期。Cypress提供对请求信息对象的访问,使你能够对其属性进行断言。此外,你甚至可以模拟和篡改请求的响应。
常见测试场景:
- 对请求体进行断言
- 对请求URL进行断言
- 对请求头进行断言
- 模拟响应体
- 模拟响应状态码
- 模拟响应头
- 延迟响应
- 等待响应发生
在Cypress中,你可以选择是模拟响应还是让请求实际到达你的服务器。你还可以在同一测试中混合使用这两种策略,选择模拟某些请求,同时让其他请求到达服务器。
让我们探讨这两种策略,为什么你会选择其中一种而不是另一种,以及为什么你应该经常同时使用两者。
使用服务器响应
未被模拟的请求实际上会到达你的服务器。通过_不_模拟响应,你正在编写真正的_端到端_测试。这意味着你以与真实用户相同的方式驱动应用程序。
当请求未被模拟时,这保证了你的客户端和服务器之间的_契约_正常工作。
换句话说,你可以确信你的服务器正在以正确的结构向客户端发送正确的数据。围绕应用程序的_关键路径_编写_端到端_测试是一个好主意。这些通常包括用户登录、注册或其他关键路径,如计费。
不模拟响应有一些缺点需要注意:
- 由于没有响应被模拟,这意味着你的服务器必须实际发送真实响应。这可能会有问题,因为你可能需要在每次测试之前_播种数据库_以生成状态。例如,如果你正在测试_分页_,你必须用生成此功能所需的所有对象来播种数据库。
- 由于真实响应会经过服务器的每一层(控制器、模型、视图等),测试通常比模拟响应慢得多。
如果你正在编写一个传统的服务器端应用程序,其中大多数响应是HTML,你可能会有很少的模拟响应。然而,大多数提供JSON的现代应用程序可以利用模拟。
- 更可能在生产环境中工作
- 对服务器端点的测试覆盖
- 非常适合传统的服务器端HTML渲染
- 需要播种数据
- 速度慢得多
- 难以测试边缘情况
- 谨慎使用
- 非常适合应用程序的_关键路径_
- 有助于围绕功能的_快乐路径_编写一个测试
模拟响应
模拟响应使你能够控制响应的各个方面,包括响应体
、状态
、头
,甚至网络延迟
。模拟非常快,大多数响应将在不到20毫秒的时间内返回。
模拟响应是控制返回给客户端数据的好方法。
你不需要在服务器上做任何工作。你的应用程序不会知道它的请求被模拟了,因此不需要_任何代码更改_。
- 控制响应体、状态和头
- 可以强制响应花费更长时间以模拟网络延迟
- 无需更改服务器或客户端代码
- 快速,响应时间< 20毫秒
- 不能保证模拟响应与实际服务器发送的数据匹配
- 某些服务器端点没有测试覆盖
- 如果你使用传统的服务器端HTML渲染,效果不佳
- 用于绝大多数测试
- 混合使用,通常有一个真正的端到端测试,然后模拟其余部分
- 非常适合JSON API
真实世界示例
Cypress 真实世界应用(RWA) 端到端测试主要依赖服务器响应,仅在少数情况下模拟网络响应,以方便创建边缘情况或难以创建的应用程序状态。
这种做法使项目能够实现应用程序前端和后端的完整代码覆盖率,但这也需要创建复杂的数据库播种或测试数据工厂脚本,以生成符合应用程序业务逻辑的适当数据。
查看真实世界应用程序测试套件中的任何一个,以了解Cypress网络处理的实际应用。
模拟
Cypress使你能够模拟响应并控制体
、状态
、头
或甚至延迟
。
cy.intercept()
用于控制HTTP请求的行为。你可以静态定义响应体、HTTP状态码、头和其他响应特性。
有关模拟响应的更多信 息和示例,请参见cy.intercept()。
路由
cy.intercept(
{
method: 'GET', // 路由所有GET请求
url: '/users/*', // 匹配URL为'/users/*'的请求
},
[] // 强制响应为: []
).as('getUsers') // 并分配一个别名
当你使用cy.intercept()
定义路由时,Cypress会在命令日志中的“路由”下显示此信息。

当新测试运行时,Cypress将恢复默认行为并删除所有路由和模拟。有关API和选项的完整参考,请参阅cy.intercept()
的文档。
固定数据
固定数据是位于文件中用于测试的一组固定数据。测试固定数据的目的是确保测试运行在一个已知且固定的环境中,从而使结果可重复。固定数据通过调用cy.fixture()
命令在测试中访问。
使用Cypress,你可以模拟网络请求并立即用固定数据响应。
在模拟响应时,你通常需要管理潜在的大而复杂的JSON对象。Cypress允许你将固定数据语法直接集成到响应中。
// 我们将响应设置为activites.json固定数据
cy.intercept('GET', '/activities/*', { fixture: 'activities.json' })
组织
Cypress在每次新项目时自动搭建出建议的文件夹结构来组织你的固定数据。默认情况下,当你将项目添加到Cypress时,它会创建一个example.json
文件。
/cypress/fixtures/example.json
你的固定数据可以在其他文件夹中进一步组织。例如,你可以创建另一个名为images
的文件夹并添加图片:
/cypress/fixtures/images/cats.png
/cypress/fixtures/images/dogs.png
/cypress/fixtures/images/birds.png
要访问嵌套在images
文件夹中的固定数据,请在cy.fixture()
命令中包含文件夹。
cy.fixture('images/dogs.png') // 以Base64形式返回dogs.png
等待
无论你是否选择模拟响应,Cypress都使你能够声明性地cy.wait()
等待请求及其响应。
以下部分使用了一个称为别名的概念。如果你是Cypress的新手,你可能想先看看这个。
以下是别名请求然后等待它们的示例:
- 端到端测试
- 组件测试
cy.intercept('/activities/*', { fixture: 'activities' }).as('getActivities')
cy.intercept('/messages/*', { fixture: 'messages' }).as('getMessages')
// 访问仪表板应发出匹配上述两个路由的请求
cy.visit('http://localhost:8888/dashboard')
// 传递一个路由别名数组,强制Cypress等待
// 直到看到每个匹配这些别名的请求的响应
cy.wait(['@getActivities', '@getMessages'])
// 这些命令在wait命令解析之前不会运行
cy.get('h1').should('contain', 'Dashboard')
cy.intercept('/activities/*', { fixture: 'activities' }).as('getActivities')
cy.intercept('/messages/*', { fixture: 'messages' }).as('getMessages')
// 挂载仪表板应发出匹配上述两个路由的请求
cy.mount(<Dashboard />)
// 传递一个路由别名数组,强制Cypress等待
// 直到看到每个匹配这些别名的请求的响应
cy.wait(['@getActivities', '@getMessages'])
// 这些命令在wait命令解析之前不会运行
cy.get('h1').should('contain', 'Dashboard')
如果你想检查别名路由的每个响应的响应数据,你可以使用多个cy.wait()
调用。
cy.intercept({
method: 'POST',
url: '/myApi',
}).as('apiCheck')
cy.visit('/')
cy.wait('@apiCheck').then((interception) => {
assert.isNotNull(interception.response.body, '第一次API调用有数据')
})
cy.wait('@apiCheck').then((interception) => {
assert.isNotNull(interception.response.body, '第二次API调用有数据')
})
cy.wait('@apiCheck').then((interception) => {
assert.isNotNull(interception.response.body, '第三次API调用有数据')
})
等待别名路由有很大的优势:
- 测试更健壮,减少了不稳定性。
- 失败消息更精确。
- 你可以对底层请求对象进行断言。
让我们探讨每个好处。
不稳定性
声明性地等待响应的一个优势是它减少了测试的不稳定性。你可以将cy.wait()
视为一个守卫,向Cypress指示你期望何时发出匹配特定路由别名的请求。这防止了下一个命令在响应返回之前运行,并防止了你的请求最初被延迟的情况。
自动完成示例:
下面这个示例的强大之处在于,Cypress会自动等待匹配getSearch
别名的请求。而不是强制Cypress测试成功请求的_副作用_(书籍结果的显示),你可以测试结果的_实际原因_。
cy.intercept('/search*', [{ item: 'Book 1' }, { item: 'Book 2' }]).as(
'getSearch'
)
// 我们的自动完成字段被节流
// 意味着它只在最后一次按键后500毫秒发出请求
cy.get('[data-testid="autocomplete"]').type('Book')
// 等待请求+响应
// 从而使我们免受节流请求的影响
cy.wait('@getSearch')
cy.get('[data-testid="results"]')
.should('contain', 'Book 1')
.and('contain', 'Book 2')
真实世界示例
Cypress 真实世界应用(RWA) 有各种测试,用于测试大型用户旅程测试中的自动完成字段,这些测试正确地等待了自动完成输入更改触发的请求。查看示例:
- 自动完成测试代码
- 自动完成测试运行视频记录
-
自动完成测试运行视频记录 在Cypress仪表板中。
失败
在我们上面的示例中,我们添加了对搜索结果显示的断言。
搜索结果的正常工作与我们的应用程序中的几个方面相关:
- 我们的应用程序向正确的URL发出请求。
- 我们的应用程序正确处理响应。
- 我们的应用程序将结果插入到DOM中。
在这个示例中,有许多可能的失败来源。在大多数测试工具中,如果我们的请求未能发出,我们通常只会在尝试在DOM中查找结果并发现没有匹配元素时得到一个错误。这是有问题的,因为不知道_为什么_结果未能显示。是我们的渲染代码有问题吗?我们是否修改或更改了元素的属性,如id
或class
?也许我们的服务器向我们发送了不同的书籍项。
使用Cypress,通过添加cy.wait()
,你可以更容易地定位特定问题。如果响应从未返回,你会收到如下错误:

现在我们确切地知道为什么我们的测试失败了。它与DOM无关。相反,我们可以看到要么我们的请求从未发出,要么请求发到了错误的URL。
断言
使用cy.wait()
等待请求的另一个好处是,它允许你访问实际的请求对象。这在你想对这个对象进行断言时非常有用。
在我们上面的示例中,我们可以对请求对象进行断言,以验证它是否在URL中作为查询字符串发送了数据。尽管我们模拟了响应,但我们仍然可以验证我们的应用程序是否发送了正确的请求。
// 任何对"/search/*"端点的请求将
// 自动接收包含两个书籍对象的数组
cy.intercept('/search/*', [{ item: 'Book 1' }, { item: 'Book 2' }]).as(
'getSearch'
)
cy.get('[data-testid="autocomplete"]').type('Book')
// 这为我们提供了拦截周期对象
// 其中包括请求和响应的字段
cy.wait('@getSearch').its('request.url').should('include', '/search?query=Book')
cy.get('[data-testid="results"]')
.should('contain', 'Book 1')
.and('contain', 'Book 2')
cy.wait()
产生的拦截对象包含了你进行断言所需的一切,包括:
- URL
- 方法
- 状态码
- 请求体
- 请求头
- 响应体
- 响应头
示例
// 监视对/users端点的POST请求
cy.intercept('POST', '/users').as('new-user')
// 通过操作web应用程序的用户界面触发网络调用,然后
cy.wait('@new-user').should('have.property', 'response.statusCode', 201)
// 我们可以再次获取完成的拦截对象
// 使用cy.get(<alias>)运行更多断言
cy.get('@new-user') // 产生相同的拦截对象
.its('request.body')
.should(
'deep.equal',
JSON.stringify({
id: '101',
firstName: 'Joe',
lastName: 'Black',
})
)
// 我们还可以在单个"should"回调中放置多个断言
cy.get('@new-user').should(({ request, response }) => {
expect(request.url).to.match(/\/users$/)
expect(request.method).to.equal('POST')
// 在expect()的第二个参数中添加断言消息是一个好习惯
expect(response.headers, '响应头').to.include({
'cache-control': 'no-cache',
expires: '-1',
'content-type': 'application/json; charset=utf-8',
location: '<domain>/users/101',
})
})
提示: 你可以通过将其记录到控制台来检查完整的请求周期对象
cy.wait('@new-user').then(console.log)
使用GraphQL
以下策略遵循等待和断言GraphQL查询或变更的最佳实践。
等待和断言GraphQL API请求依赖于匹配POST体中的查询或变更名称。
使用cy.intercept(),我们可以通过在测试开始时或接近期望时声明拦截来覆盖GraphQL查询或变更的响应。
别名多个查询或变更
在beforeEach
中,我们将使用cy.intercept()捕获对GraphQL端点(例如/graphql
)的所有请求,使用条件匹配查询或变更,并使用req.alias
设置别名。
首先,我们将创建一组实用函数来帮助匹配和别名我们的查询和变更。
// utils/graphql-test-utils.js
// 根据操作名称匹配GraphQL变更的实用函数
export const hasOperationName = (req, operationName) => {
const { body } = req
return (
Object.prototype.hasOwnProperty.call(body, 'operationName') &&
body.operationName === operationName
)
}
// 如果operationName匹配,则别名查询
export const aliasQuery = (req, operationName) => {
if (hasOperationName(req, operationName)) {
req.alias = `gql${operationName}Query`
}
}
// 如果operationName匹配,则别名变更
export const aliasMutation = (req, operationName) => {
if (hasOperationName(req, operationName)) {
req.alias = `gql${operationName}Mutation`
}
}
在我们的测试文件中,我们可以导入这些实用函数并在beforeEach
中使用它们来为我们的测试别名查询和变更。
// app.cy.js
import { aliasQuery, aliasMutation } from '../utils/graphql-test-utils'
context('测试', () => {
beforeEach(() => {
cy.intercept('POST', 'http://localhost:3000/graphql', (req) => {
// 查询
aliasQuery(req, 'GetLaunchList')
aliasQuery(req, 'LaunchDetails')
aliasQuery(req, 'GetMyTrips')
// 变更
aliasMutation(req, 'Login')
aliasMutation(req, 'BookTrips')
})
})
// ...
})
查询或变更结果的期望
可以使用cy.wait()对拦截的GraphQL查询或变更的响应进行期望。
// app.cy.js
import { aliasQuery } from '../utils/graphql-test-utils'
context('测试', () => {
beforeEach(() => {
cy.intercept('POST', 'http://localhost:3000/graphql', (req) => {
// 查询
aliasQuery(req, 'Login')
// ...
})
})
it('应验证登录数据', () => {
cy.wait('@gqlLoginQuery')
.its('response.body.data.login')
.should('have.property', 'id')
.and('have.property', 'token')
})
})
修改查询或变更响应
在下面的测试中,修改响应以测试单页结果的UI。
// app.cy.js
import { hasOperationName, aliasQuery } from '../utils/graphql-test-utils'
context('测试', () => {
beforeEach(() => {
cy.intercept('POST', 'http://localhost:3000/graphql', (req) => {
// 查询
aliasQuery(req, 'GetLaunchList')
// ...
})
})
it('在启动页面不应显示加载更多按钮', () => {
cy.intercept('POST', 'http://localhost:3000/graphql', (req) => {
if (hasOperationName(req, 'GetLaunchList')) {
// 从beforeEach中的初始拦截声明别名
req.alias = 'gqlGetLaunchListQuery'
// 使用req.fixture或req.reply修改部分响应
req.reply((res) => {
// 直接修改响应体
res.body.data.launches.hasMore = false
res.body.data.launches.launches =
res.body.data.launches.launches.slice(5)
})
}
})
// 必须在cy.intercept之后访问
cy.visit('/')
cy.wait('@gqlGetLaunchListQuery')
.its('response.body.data.launches')
.should((launches) => {
expect(launches.hasMore).to.be.false
expect(launches.length).to.be.lte(20)
})
cy.get('#launch-list').its('length').should('be.gte', 1).and('be.lt', 20)
cy.contains('button', '加载更多').should('not.exist')
})
})
命令日志
默认情况下,Cypress在命令日志中记录被测应用程序发出的所有XMLHttpRequest
和fetch
请求。以下是一个示例:

注意: 可以通过在静态响应中传递{ log: false }
来禁用日志记录。
cy.intercept('/users*', { body: ['user1', 'user2'], log: false }).as('getUsers')
要禁用所有xhr/fetch请求的日志,请查看我们在intercept api页面上的示例。
左侧的圆形指示器指示请求是否到达了目标服务器。如果圆圈是实心的,请求到达了目标服务器;如果是空心的,响应被cy.intercept()
模拟且未发出。
如果我们重新运行之前的测试以发出相同的请求,但这次添加一个cy.intercept()
来模拟对/users
的响应,我们可以看到指示器发生了变化。添加以下行后:
cy.intercept('/users*', ['user1', 'user2']).as('getUsers')
命令日志将如下所示:

fetch
请求现在有一个空心圆圈,表示它已被模拟。另外,注意cy.intercept()
的别名现在显示在命令日志的右侧。如果将鼠标悬停在别名上,你可以看到有关如何处理请求的更多信息:

此外,如果请求和/或响应被cy.intercept()
处理函数修改,请求将被标记。如果我们添加以下代码来修改对/users
的传出请求:
cy.intercept('/users*', (req) => {
req.headers['authorization'] = 'bearer my-bearer-auth-token'
}).as('addAuthHeader')
/users
的请求日志将反映req
对象已被修改,但请求仍由目标服务器完成(实心指示器):

如你所见,“req modified”显示在徽章中,表示请求对象已被修改。“res modified”和“req + res modified”也可以显示,具体取决于res
是否在req.continue()
回调中被修改。
与所有命令日志一样,可以通过单击网络请求的日志在控制台中显示其他信息。例如,在单击之前的/users?limit=100
请求并打开开发者工具后,我们可以看到以下内容:

另请参阅
cy.intercept()
文档- Kitchen Sink示例中的网络请求
- 了解如何使用
cy.request()
发起请求 - 真实世界应用程序(RWA)测试套件,了解Cypress网络处理的实际应用。
- 阅读博客文章从Cypress测试中断言网络调用
- 如果你想在离线模式下测试应用程序,请阅读在离线网络模式下测试应用程序