你已经使用 Cypress 运行端到端测试了。现在你想直接调用 API、检查状态码并对 JSON body 进行断言,而无需通过 UI 界面进行点击操作。Cypress 完全可以做到这一点。cy.request() 命令从浏览器外部的 Node 进程发送真实的 HTTP 请求,因此你可以单独测试某个接口,或者在 UI 测试运行之前进行状态初始化。
本指南将向你展示如何使用 Cypress 进行 API 测试:包括 cy.request() 的基础用法、对响应进行断言、链式调用请求以复用 auth Token,以及使用 cy.intercept() 拦截和模拟浏览器流量。你还将了解到 Cypress 在 API 测试中的适用场景,以及在什么情况下使用原生 API 工具更为合适。
为什么使用 Cypress 测试 API
大多数 Cypress 测试套件都是驱动浏览器运行的。但你在 UI 中验证的很多内容,本质上都是底层的 API:例如 /login 是否返回了 Token,/users 返回的数据结构是否正确,POST 请求是否创建了预期的记录。
直接测试这些接口有两个好处。首先,API 检查的运行速度比 UI 流程更快,因为没有页面渲染,也不需要等待元素加载。其次,你可以使用 cy.request() 来初始化测试数据。无需通过点击注册表单来创建用户,只需发送一个 POST 请求,然后直接进入你真正关心的断言步骤。
如果你的技术栈中已经包含了 Cypress,那么添加 API 测试就意味着无需安装任何新工具。你可以在同一个测试文件中编写它们,使用相同的 expect 断言,并在同一个 CI 流程中运行。
cy.request() 基础知识
cy.request() 用于发送 HTTP 请求并获取响应。它在 Cypress 的 Node 进程中运行,而不是在浏览器中运行,因此不受 CORS 或同源策略的限制。
该命令支持以下几种调用形式:
cy.request(url)
cy.request(url, body)
cy.request(method, url)
cy.request(method, url, body)
cy.request(options)
最简单的调用方式是传入一个 URL。如果没有指定方法,Cypress 默认会使用 GET:
cy.request('https://jsonplaceholder.typicode.com/users')
对于除基本 GET 之外的其他操作,可以传入一个 options object。这让你能够完全控制方法、body 和 headers:
cy.request({
method: 'POST',
url: 'https://jsonplaceholder.typicode.com/posts',
headers: {
'Content-Type': 'application/json'
},
body: {
title: 'API test',
body: 'created from Cypress',
userId: 1
}
})
该 object 中常用的配置项包括:
method:HTTP 请求方法(GET、POST、PUT、PATCH、DELETE等)。默认为GET。url:接口地址。必填项。body:请求的 payload。Cypress 会自动将对象序列化为 JSON。headers:请求 header 的 object。auth:用于 HTTP 基础认证(Basic Authentication)的凭证。qs:以 object 形式表示的查询字符串参数(query string parameters)。failOnStatusCode:当你想要对 4xx 或 5xx 响应进行断言而不是直接让测试失败时,将此项设置为false。
最后一个选项对于异常测试非常重要。默认情况下,如果收到任何非 2xx 或 3xx 的状态码,cy.request() 都会使测试失败。如果你要验证一个错误的请求是否返回 400,你需要禁用此默认行为:
cy.request({
method: 'GET',
url: 'https://jsonplaceholder.typicode.com/users/99999',
failOnStatusCode: false
}).then((response) => {
expect(response.status).to.eq(404)
})
断言 status 和 body
cy.request() 会返回一个响应对象。可以通过链式调用 .then() 来读取它。该响应包含以下四个最常用的属性:
status:HTTP 状态码。body:响应 payload。当Content-Type以json结尾时,Cypress 会自动将其解析为 JavaScript 对象。headers:响应 header。duration:请求耗时(毫秒)。
以下是一个完整的测试,用于验证 status、body 结构以及特定字段:
describe('Users API', () => {
it('returns a list of users', () => {
cy.request('https://jsonplaceholder.typicode.com/users').then((response) => {
expect(response.status).to.eq(200)
expect(response.body).to.be.an('array')
expect(response.body).to.have.length(10)
expect(response.body[0]).to.have.property('email')
})
})
})
由于 response.body 已经是一个解析后的对象,你可以使用标准的 Chai 对其进行断言。你可以验证类型、长度、嵌套属性或具体的值。这与你用于 UI 测试的断言语法完全相同,因此无需学习新概念。
你也可以对响应 header 进行断言,这在验证内容类型或缓存设置时非常有用:
cy.request('https://jsonplaceholder.typicode.com/users').then((response) => {
expect(response.headers['content-type']).to.include('application/json')
})
欲深入了解如何构建这些校验,请参阅我们的 API 断言指南以及更广泛的 API 测试最佳实践。
链式请求与复用 auth Token
真实的 API 通常需要身份验证。常见的模式是登录一次,获取 Token,然后在其后的每个请求中发送该 Token。cy.request() 的链式调用非常适合处理这种场景。
发送登录请求,从响应 body 中读取 Token,然后在下一次调用中使用它:
describe('Authenticated API flow', () => {
it('logs in and fetches a protected resource', () => {
cy.request({
method: 'POST',
url: 'https://api.example.com/login',
body: {
email: 'user@example.com',
password: 'secret'
}
}).then((loginResponse) => {
expect(loginResponse.status).to.eq(200)
const token = loginResponse.body.token
cy.request({
method: 'GET',
url: 'https://api.example.com/profile',
headers: {
Authorization: `Bearer ${token}`
}
}).then((profileResponse) => {
expect(profileResponse.status).to.eq(200)
expect(profileResponse.body).to.have.property('email', 'user@example.com')
})
})
})
})
如果多个测试需要相同的 Token,可以将登录逻辑移入 beforeEach 中,并使用 Cypress.env() 或封装的别名(alias)来存储 Token,以便每个测试都能获取到它。这样可以将 auth 设置集中在一处,而无需在每个 spec 中重复编写。
这种“先登录后使用”的模式与你在编写任何 API 集成测试流程时的逻辑完全一致,即一个调用产生的数据会传递给下一个调用。
使用 cy.intercept() 进行存根模拟
cy.request() 会向真实的 API 发送请求。有时你可能需要相反的操作:拦截前端应用发出的请求,并返回一个虚假的响应。这就是 cy.intercept() 的作用。
这里有一个重要的区别。cy.intercept() 仅拦截前端应用在浏览器中发出的请求。它不会拦截 cy.request(),因为 cy.request() 完全绕过了浏览器。当你需要测试 UI 如何对给定的 API 响应做出反应时,请使用 cy.intercept(),而不是在测试 API 本身时使用。
你可以监听(spy)请求、使用静态响应进行模拟(stub),或者等待它。要进行模拟,只需传入一个响应 object:
javascript cy.intercept('GET', '/api/users', { statusCode: 200, body: [{ id: 1, name: 'Ada' }] })
现在,任何浏览器对 /api/users 的请求都将返回这个固定的 payload。这使你能够测试在真实后端环境下难以触发的边缘情况,例如空列表、500 错误或慢响应。
要断言该请求已发生,可以使用 .as() 为其设置别名并等待它:
javascript it('shows an error banner when the API fails', () => { cy.intercept('GET', '/api/users', { statusCode: 500, body: { message: 'Server error' } }).as('getUsers')
cy.visit('/dashboard') cy.wait('@getUsers').its('response.statusCode').should('eq', 500) cy.contains('Something went wrong').should('be.visible') })
cy.wait('@getUsers') 这行代码会暂停测试,直到拦截的请求完成解析,然后将请求和响应传给你以进行断言。请务必在触发请求的操作之前设置拦截,否则它将无法捕获任何内容。如果你正在权衡何时模拟响应与 mock 整个服务,我们关于 API mocking 以及 API stubbing vs API mocking 的文章中详细分析了各自的利弊。
何时适合使用 Cypress 进行 API 测试,何时不适合
Cypress 非常适合嵌入在 UI 测试套件中的 API 检查。如果你已经在编写端到端测试,并且需要播种(seed)数据、验证 UI 依赖的接口,或者通过模拟响应来测试错误处理,cy.request() 和 cy.intercept() 可以将所有内容集中在同一个地方。无需切换上下文,也无需引入第二个工具。
如果 Cypress 是你唯一的测试 runner,并且你不想添加其他依赖项,那么用它来进行少量独立的 API 冒烟测试也是完全可以的。
但如果是针对大型的、独立的 API 测试套件,情况就会变得有些尴尬。Cypress 是专为浏览器构建的,因此 API 测试只是在其上附加的一项功能,而非核心设计。随着测试套件的增长,会出现一些不足之处:
- 没有可视化的请求构建器。每个请求都是代码。当你有数百个接口时,手动编写 header、body 和 query 参数会变得很慢。
- 对于仅在 CI 中运行的 API 流水线来说较为逊色。Cypress 即使对于纯 API 运行也会启动浏览器上下文,这会带来不必要的开销。
- 没有共享的 API 定义。请求的结构存在于你的测试文件中,而不是存在于团队可以在设计、文档和测试中复用的规范中。
这就是 API 原生工具更合适的地方。Apifox 是围绕 API 本身构建的:你只需设计一次接口,然后使用可视化请求构建器和可视化断言来针对它构建测试场景,常见情况下无需编写代码。请求、环境和 auth 都保存在共享工作区中,因此你的团队无需从测试文件中重新推导它们。
对于 CI,Apifox CLI 可以在任何能运行 Node 的步骤中无头运行你保存的测试场景:
npm install -g apifox-cli
apifox run \ --access-token "$APIFOXACCESSTOKEN" \ -t\ -e\ -r cli,html,junit -t 参数指向已保存的场景或套件,-e 选择一个环境,而 -r 挑选报告格式(cli、html、json、junit)。JUnit 输出可以直接接入大多数 CI 仪表盘。添加 -d 或 --iteration-data 可以使用数据文件来驱动测试,而 --upload-report 可以将结果推送回你的工作区。CLI 运行的是已保存的场景,它不是一个交互式的请求发送器。
一个好记的思考方式是:对于支持浏览器测试的 API 检查,使用 cy.request();当 API 测试是主要工作时,选择 API 原生工具。如果你正在对比各种选择,我们收集的在 CI/CD 中运行的 API 测试自动化工具以及更广泛的 30 个最佳 API 测试工具涵盖了这一领域。
常见问题
cy.request() 是在浏览器中运行吗?
不。cy.request() 运行在 Cypress Node 进程中,处于浏览器外部。这就是为什么它不会被 CORS 或同源策略阻止,以及为什么 cy.intercept() 无法拦截它的原因。如果你需要浏览器发出的请求,请通过你的应用触发它并使用 cy.intercept() 进行捕获。
如何在不导致测试失败的情况下测试 400 或 404 响应?
默认情况下,cy.request() 会在任何非 2xx 或非 3xx 状态码时报错导致测试失败。请在选项对象中设置 failOnStatusCode: false,然后自己对状态进行断言,例如 expect(response.status).to.eq(404)。
我可以使用 cy.request() 在 UI 测试之前进行登录吗?
可以,而且这是一个很常见的模式。使用 cy.request() 向你的登录接口发送一个 POST 请求,从 response.body 中读取 Token,并将其存储(在 Cypress.env()、localStorage 或 cookie 中),这样你的 UI 测试在开始时就已经完成了身份验证。这可以跳过登录表单并提高套件的运行速度。
cy.request() 和 cy.intercept() 之间有什么区别?
cy.request() 发送真实的 HTTP 请求并返回实际的响应,因此你可以用它来测试 API。cy.intercept() 则是监听或模拟前端在浏览器中发出的请求,因此你可以用它来控制 UI 接收到的内容。前者直接访问网络,后者则对网络请求进行拦截和塑造。
我应该使用 Cypress 还是专门的 API 测试工具?
当 API 检查是为了辅助你现有的浏览器测试套件时,可以使用 Cypress。但对于大型的独立 API 套件、仅在 CI 运行的 API 流水线,或者整个团队共同协作的共享 API 定义,像 Apifox 这样原生支持 API 的工具会更加合适。请参阅我们的 API 测试最佳实践以了解如何进行选择。
开发必备:API 全流程管理神器 Apifox
介绍完上文的内容,我想额外介绍一个对开发者同样重要的效率工具 —— Apifox。作为一个集 API 文档、调试、设计、测试、Mock、自动化测试于一体的工具,Apifox 是目前提升研发效率的首选。
如果你正在开发项目,不妨试试其极其友好的界面设计,它完全兼容 Postman 和 Swagger 数据格式,导入数据非常方便,,即使是新手也能很快上手,点击这里即可注册使用。

值得一提的是,除了个人和常规团队使用,针对有高安全合规要求、或需要在内网环境协作的企业,Apifox 还提供了深度定制的私有化部署方案。
获取专属报价与部署方案
详细的私有化部署系统架构与安全白皮书
针对您公司规模的专属报价单
免费的 1v1 专属产品演示 (Demo) 机会