5月28日 00:27

在 Cypress 中如何处理异步操作和 Promise?

在 Cypress 测试中,几乎所有操作都是异步的——无论是查找元素、发起请求还是等待页面渲染。很多开发者习惯性地把 Cypress 命令当作同步代码来写,结果变量拿到的是 Chainable 对象而非实际值,测试时灵时不灵。理解 Cypress 的异步机制并正确处理 Promise,是写好端到端测试的关键。

Cypress 命令为什么不返回值

Cypress 的每一条命令(如 cy.get()cy.contains())都不会立即执行,而是被放入一个命令队列(command queue)。当测试运行时,Cypress 按顺序依次执行队列中的命令,每条命令返回的是一个 Chainable 对象,而不是实际的 DOM 元素或数据。

javascript
// 常见错误:试图把 cy.get() 的返回值当同步数据用 const text = cy.get('.title').invoke('text'); // text 是 Chainable,不是字符串 console.log(text); // 输出的是 Chainable 对象,不是文本内容

这就是为什么不能在 Cypress 中使用 const 直接获取命令结果。必须通过 .then() 回调来访问实际值。

为什么不能在 Cypress 中使用 async/await

这是面试高频考点。Cypress 命令不是标准的 JavaScript Promise,不能用 await 等待:

javascript
// 这样写无法正常工作 it('错误示范', async () => { const $el = await cy.get('.btn'); // cy.get() 不返回 Promise const text = await $el.text(); });

Cypress 的命令通过内部队列管理执行顺序,而不是通过 Promise 链。async/await 会破坏这个队列机制,导致命令执行顺序混乱。正确做法是使用 .then() 链式调用。

用 .then() 处理异步结果

.then() 是 Cypress 中获取前一条命令实际返回值的标准方式:

javascript
cy.get('.user-name').then(($el) => { // $el 是 jQuery 对象,可以同步操作 const text = $el.text(); expect(text).to.include('管理员'); });

.then() 回调中,你拿到的是真实数据,可以进行同步操作和断言。需要注意的是,回调中的同步代码会阻塞后续命令,因此不要在回调里放耗时操作。

如果需要在 .then() 中返回 Cypress 命令,可以返回 Chainable 对象,Cypress 会自动解包:

javascript
cy.get('.user-id').invoke('text').then((id) => { // 返回 cy.request,Cypress 会等待请求完成 return cy.request(`/api/users/${id}`); }).then((response) => { expect(response.status).to.eq(200); });

用 cy.wrap() 将值纳入命令队列

当你需要把一个普通值或第三方 Promise 引入 Cypress 命令链时,使用 cy.wrap()

javascript
// 包装同步值 const data = { name: '张三', role: 'admin' }; cy.wrap(data).its('name').should('eq', '张三'); // 包装第三方 Promise function fetchConfig() { return new Promise((resolve) => { resolve({ theme: 'dark' }); }); } cy.wrap(fetchConfig()).its('theme').should('eq', 'dark');

cy.wrap() 会等待被包装的 Promise 解析完成后,再继续执行后续命令。这意味着 Cypress 的重试和超时机制会生效。

一个典型场景:在 .then() 回调中拿到值后,需要用 .should() 做断言,但 .should() 需要 Chainable 上下文,这时用 cy.wrap() 桥接:

javascript
cy.get('.count').invoke('text').then((text) => { const count = parseInt(text, 10); cy.wrap(count).should('be.greaterThan', 0); });

用 cy.request() 处理 API 请求

cy.request() 直接发起 HTTP 请求,返回响应数据,无需通过浏览器界面:

javascript
cy.request('POST', '/api/login', { username: 'admin', password: '123456' }).then((response) => { expect(response.status).to.eq(200); expect(response.body.token).to.exist; });

在测试前置准备中,用 cy.request() 代替界面操作可以显著加快测试速度。比如创建测试数据、设置登录状态等。结合 Cypress.Promise 构造函数,可以封装更复杂的异步前置逻辑:

javascript
beforeEach(() => { cy.request('POST', '/api/login', credentials).then((res) => { // 将 token 存为别名,后续测试可直接访问 cy.wrap(res.body.token).as('authToken'); }); }); it('携带 token 请求受保护接口', function () { cy.request({ url: '/api/profile', headers: { Authorization: `Bearer ${this.authToken}` } }).then((res) => { expect(res.status).to.eq(200); }); });

用 cy.intercept() 和 cy.wait() 管控网络请求

cy.intercept() 拦截和模拟网络请求,cy.wait() 等待请求完成,两者配合使用是处理异步网络操作的核心模式:

javascript
// 拦截请求并设置别名 cy.intercept('GET', '/api/users').as('getUsers'); // 触发请求的操作 cy.get('.refresh-btn').click(); // 等待请求完成后再验证 cy.wait('@getUsers').then((interception) => { expect(interception.response.statusCode).to.eq(200); expect(interception.response.body).to.have.property('list'); });

这种方式比硬编码 cy.wait(2000) 可靠得多。cy.wait('@alias') 会等待实际的请求完成,不会因为网络波动导致测试失败,也不会因为等待过久而浪费时间。

cy.intercept() 还能模拟后端响应,让测试不依赖真实 API:

javascript
cy.intercept('GET', '/api/users', { statusCode: 200, body: [{ id: 1, name: '测试用户' }] }).as('mockUsers'); cy.visit('/users'); cy.wait('@mockUsers'); cy.get('.user-list').should('contain', '测试用户');

处理多个并发 Promise

当需要同时等待多个异步操作时,可以用 Cypress.Promise.all()

javascript
Cypress.Promise.all([ cy.request('/api/config'), cy.request('/api/userinfo') ]).then(([configRes, userRes]) => { expect(configRes.status).to.eq(200); expect(userRes.status).to.eq(200); });

注意这和 Promise.all() 不同——Cypress.Promise.all() 返回的对象可以继续链式调用 Cypress 命令。

常见陷阱与排查

在 .then() 回调中调用 cy 命令

.then() 回调中可以调用 cy 命令,它们会被追加到命令队列末尾,而不是立即执行:

javascript
cy.get('.btn').then(($btn) => { // 这里的 cy 命令不是同步执行的 cy.get('.result').should('contain', '成功'); // 如果依赖 $btn 的状态做后续操作,要确保逻辑在回调内完成 });

闭包变量丢失

javascript
let userName; cy.get('.name').then(($el) => { userName = $el.text(); }); // 这里 userName 还是 undefined,因为 cy.get() 还没执行 cy.log(userName); // undefined

正确做法是将后续操作放在 .then() 链中:

javascript
cy.get('.name').invoke('text').then((name) => { cy.log(name); // 能正确输出 cy.get('.greeting').should('contain', name); });

混用 jQuery 同步方法与 Cypress 异步命令

Cypress.$() 是同步的 jQuery 选择器,不会重试也不会等待:

javascript
// 同步,元素不存在时直接返回空集合,不会重试 const $el = Cypress.$('.dynamic-content'); if ($el.length) { /* 可能永远不执行 */ } // 异步,会自动重试直到元素出现或超时 cy.get('.dynamic-content').should('be.visible');

除非有明确理由,否则优先使用 cy.get() 而非 Cypress.$()

追问:Cypress 如何实现命令的自动重试?

Cypress 在执行断言时,如果当前命令的结果不满足断言条件,会自动重新执行该命令(而不是抛出错误),直到超时。这个机制只对查询类命令生效(如 cy.get()cy.contains().should()),对动作类命令(如 .click().type())不生效。理解这一点有助于判断哪些场景需要手动添加 .should() 显式等待。

标签:Cypress