标签

Cypress

Cypress 是一个前端自动化测试工具,用于测试基于Web的应用程序。它能够测试运行在浏览器中的应用,并且适用于单元测试、集成测试和端到端(E2E)测试。Cypress 提供了一个丰富的API集,以及一个友好的交互式界面,让开发和测试人员能够轻松编写、运行和调试测试用例。

Cypress
查看更多相关内容
服务端5月29日 01:09
如何配置 Cypress 测试报告和 CI/CD 集成?Cypress 测试报告配置分两步:选 reporter、配参数。最常用的是 Mochawesome,在 cypress.config.js 中设 reporter 为 'mochawesome',通过 reporterOptions 指定 reportDir、overwrite: false、html: true、chart: true。如需合并多个 spec 的报告,搭配 mochawesome-merge 工具合并 JSON 再生成单份 HTML。CI/CD 集成的关键是:用 `npx cypress run --reporter mochawesome` 在无头模式执行;通过 `--parallel` 参数配合 Cypress Cloud 实现并行测试加速;用 `actions/upload-artifact` 收集报告和失败时的截图/视频;在 workflow 触发条件中绑定 push/pull_request 事件。失败截图和视频默认保存在 cypress/screenshots 和 cypress/videos 目录,CI 中应作为 artifact 上传以便排查。 ## 追问 - mochawesome-merge 的作用是什么?为什么多个 spec 会生成多份报告? - Cypress 的 `--parallel` 参数如何工作?不使用 Cypress Cloud 能实现并行吗? - 如何在 CI 中只在测试失败时才上传视频和截图? - Allure 报告和 Mochawesome 相比各有什么优劣?什么场景该选 Allure? - 如何在 GitHub Actions 中设置定时跑 Cypress 测试(cron 触发)? ## 写段代码 ```yaml # .github/workflows/cypress.yml name: Cypress on: [push] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: npm ci - run: npx cypress run --reporter mochawesome - uses: actions/upload-artifact@v4 if: always() with: name: report path: cypress/results ```
服务端5月29日 00:52
Cypress 如何处理异步操作?命令链和自动等待机制是什么?Cypress 的命令不是立即执行,而是入队后按序串行执行。每个命令返回 chainable 对象,后续命令挂载到链条上形成命令队列,Cypress 依次取出执行并自动等待前置条件满足。自动等待指每个命令内建重试机制:cy.get() 会反复查询 DOM 直到元素存在且可见,cy.request() 会等待响应返回,默认超时 4 秒。开发者无需写 sleep 或显式等待,Cypress 在命令间自动处理异步时序。 ## 追问 **命令队列和 Promise 链有什么区别?** 命令队列在 .then() 之前不会执行,是同步入队异步执行;Promise 链是立即执行。所以不能把 Cypress 命令赋值给变量:const el = cy.get('#btn') 拿到的是 chainable 不是元素,必须用 .then() 回调取值。 **什么时候需要用 .then()?** 需要访问命令返回值或混合同步逻辑时。比如从响应中提取 ID 再构造下一个请求。注意 .then() 内部的 cy 命令会重新入队,不会立即执行。 **自动等待超时了怎么办?** 可通过 { timeout: 10000 } 单独设置,或在 cypress.config.js 中配置 defaultCommandTimeout 全局调整。超时后命令失败,测试中断并截图。应优先用 should() 断言替代加大超时。 **cy.wait() 和自动等待什么时候用?** 自动等待覆盖 DOM 和 XHR 场景,一般够用。但 cy.intercept() 拦截请求后需 cy.wait('@alias') 确保请求完成再断言响应,这是显式等待的典型场景。 **为什么不能在 .then() 外用 async/await?** Cypress 命令不在 Promise 上运行,await 一个 chainable 不会等命令执行完。混用 async/await 会导致时序错乱,Cypress 官方明确不推荐在命令链中使用 async/await。 ## 写段代码 ```javascript cy.intercept('GET', '/api/users').as('users'); cy.visit('/dashboard'); cy.wait('@users').its('response.statusCode').should('eq', 200); cy.get('#user-list').should('be.visible'); ```
服务端5月29日 00:52
Cypress 和 Selenium 有什么区别?何时选择 Cypress?核心区别在架构:Cypress 运行在与应用同源的浏览器内,通过 Chrome DevTools Protocol 直接操作 DOM,内置自动等待和重试机制;Selenium 通过外部 WebDriver 进程与浏览器通信,需显式编写等待逻辑。这意味着 Cypress 调试体验远优于 Selenium(可视化 Test Runner、时间旅行),且代码更简洁,但仅支持 Chromium 内核和 JavaScript;Selenium 跨浏览器覆盖全面(Chrome/Firefox/Safari),支持多语言(Java/Python/C#),适合需要兼容性测试的团队。选择 Cypress 的场景:前端 SPA 项目为主、团队用 JavaScript、追求快速反馈和低维护成本。选 Selenium 的场景:必须覆盖多浏览器、团队非 JS 技术栈、需测试非 Web 应用。 ## 追问 - Cypress 的同源架构为什么无法测试跨域场景?有什么变通方案? - Selenium 的显式等待(WebDriverWait)和隐式等待(implicit wait)有什么区别?各自的风险是什么? - Cypress 的 cy.intercept() 如何模拟后端响应?与 Selenium 的 Mock Server 方案相比优劣如何? - 大型项目中 Cypress 测试执行变慢,如何优化? - Playwright 与 Cypress 相比有哪些改进?是否正在取代 Cypress? ## 写段代码 ```javascript // Cypress: 自动等待,无需 sleep cy.visit('/login'); cy.get('#user').type('admin'); cy.get('#pass').type('1234'); cy.get('#submit').click(); cy.url().should('include', '/dashboard'); // Selenium (Python): 必须显式等待 from selenium.webdriver.support.ui import WebDriverWait elem = WebDriverWait(driver, 10).until( EC.element_to_be_clickable((By.ID, 'submit')) ) elem.click() ```
服务端5月29日 00:25
Cypress 中如何实现数据驱动测试?数据驱动测试将测试数据与逻辑分离,Cypress 通过 cy.fixture() 加载 cypress/fixtures/ 下的 JSON 文件驱动测试。核心流程:在 fixtures 目录建数据文件,测试中用 cy.fixture() 加载后遍历执行,实现一组逻辑跑多组数据。更简洁的方式是结合 cy.each() 或原生 forEach 迭代数据,避免为每组数据写重复测试。外部数据文件适合管理多环境配置和边界值数据集,fixtures 适合静态模拟数据。 ## 追问 **cy.fixture() 和直接 import JSON 有什么区别?** cy.fixture() 走 Cypress 管道,支持超时重试和命令日志;import 是编译时加载,不经过 Cypress 命令链,无法在报告中追踪。 **如何用 fixtures 实现参数化测试?** 用 cy.fixture() 加载数组数据,配合 cy.each() 或 forEach 遍历,每组数据生成独立 it 用例,失败时能精确定位是哪组数据有问题。 **fixtures 数据在不同测试间会互相影响吗?** Cypress 默认每个测试前重置 fixtures 状态;但如果在 before 中修改 fixture 返回的对象,会影响后续测试,建议每次加载用深拷贝。 **大量测试数据该怎么管理?** 按模块分目录(fixtures/auth/、fixtures/products/),公共数据放 fixtures/common/;环境相关数据用 cypress.env.json + CYPRESS_ 环境变量区分。 ## 写段代码 ```javascript // fixtures/users.json: [{"name":"Alice","role":"admin"},{"name":"Bob","role":"user"}] describe('数据驱动权限测试', () => { let users; before(() => { cy.fixture('users').then(data => users = data); }); users.forEach((user, i) => { it(`用户 ${user.name} 角色为 ${user.role}`, () => { cy.login(user.name, 'pass'); cy.get('[data-testid=role]').should('contain', user.role); }); }); }); ```
服务端5月29日 00:24
Cypress 中 Page Object 模式有必要用吗?Page Object 模式将页面元素选择器和操作封装为独立类,测试代码只调用方法不直接写选择器,页面变更时只需改 Page Object 不改测试。但在 Cypress 中,Custom Command 常能替代 POM 的大部分功能——cy.login() 比 loginPage.login() 更符合 Cypress 风格。POM 真正有价值的场景是:多页面复杂流程(如电商下单流程跨 4 个页面)、团队已熟悉 POM 模式、选择器需要跨多个测试文件共享复用。 ## 追问 **Cypress 官方对 POM 的态度是什么?** 官方认为 POM 不是必须的,Cypress 的 Custom Command 和组合式 API 已能很好复用逻辑;过度封装反而增加维护成本,简单场景用 Custom Command 更合适。 **Custom Command 和 POM 怎么选?** 单页面或少交互场景用 Custom Command(如 cy.login());多页面流程且团队习惯 OOP 风格时用 POM,两者可混合使用。 **POM 中选择器应该怎么管理?** 统一使用 data-testid 属性作为选择器锚点,不依赖 CSS class 或 DOM 结构,UI 样式变更不影响测试稳定性。 **POM 类变得臃肿怎么办?** 拆分为基础 PageObject(通用方法)+ 具体页面子类;组件级别的对象(如导航栏)独立为 Component Object,避免单类膨胀。 ## 写段代码 ```javascript // POM 类 + 测试使用 class LoginPage { get username() { return cy.get('[data-testid=username]'); } get password() { return cy.get('[data-testid=password]'); } login(user, pass) { this.username.type(user); this.password.type(pass); cy.get('[data-testid=submit]').click(); } } // 测试中 const login = new LoginPage(); login.login('admin', '123456'); cy.url().should('include', '/dashboard'); ```
服务端5月29日 00:24
如何优化 Cypress 测试的执行速度?核心优化手段:用 cy.session() 缓存登录状态避免重复登录;通过 --parallel 并行执行拆分 spec 文件;用 cy.intercept() 拦截 mock 网络请求减少真实 API 调用;避免 cy.wait() 硬编码等待,让 Cypress 自动重试机制生效;配置 baseUrl 避免重复导航。综合使用可将 1000+ 用例执行时间从 20 分钟压到 5 分钟以内。 ## 追问 **cy.session() 和 before() 中登录有什么区别?** before() 每个测试文件都会执行登录;cy.session() 在同文件内跨测试复用登录状态,且 session 失效时自动重建,减少冗余请求。 **并行执行为什么需要 Cypress Cloud?** Cypress 的并行调度依赖 Dashboard 服务分配测试到不同机器,免费版可用 cypress-parallel 插件做本地并行,但缺少负载均衡。 **如何识别最慢的测试用例?** 运行 cypress run --reporter=json 生成报告,按 duration 排序定位瓶颈;或在 Cypress Cloud 查看耗时分布图。 **spec 文件应该怎么拆分?** 按功能模块拆分,每个 spec 控制在 10-20 个测试;避免单文件过大影响并行均衡,也避免过碎导致启动开销占比过高。 **cy.intercept() mock 数据会不会导致测试失真?** 会,应在关键流程用真实 API,仅在辅助请求(如第三方服务)使用 mock,并在 CI 中定期跑无 mock 的全量回归验证。 ## 写段代码 ```javascript // cy.session 缓存登录 + intercept mock beforeEach(() => { cy.session('user', () => { cy.intercept('POST', '/api/login', { statusCode: 200 }); cy.visit('/login'); cy.get('[name=email]').type('user@test.com'); cy.get('[name=password]').type('pass123'); cy.get('button').click(); }); }); ```
服务端5月29日 00:24
Cypress 测试隔离和数据管理怎么做?Cypress 默认每个 it 块前会重置浏览器状态(清 cookie、localStorage、sessionStorage),Cypress 12+ 开启 `testIsolation: true` 后更强——每次测试前自动 `cy.visit()` 恢复初始页面。数据管理分三层:fixtures 管理静态数据、cy.request() + 自定义命令做动态数据创建、cy.task() 操作数据库清理。核心原则:测试不依赖执行顺序,每个测试自给自足。 ## 追问 **cy.session() 怎么用?和 beforeEach 中登录有什么区别?** cy.session() 缓存登录后的 cookie 和 localStorage,同一 spec 内重复使用时不重新登录,显著加速测试。而 beforeEach 每次都执行完整登录流程。session 在 spec 间不共享(Cypress 12+ 的限制),跨 spec 需配合 cy.request 预置状态。 **testIsolation: true 和 false 各适合什么场景?** true(默认)适合功能测试,保证每个用例干净状态;false 适合需要跨测试保持状态的端到端流程测试(如多步向导),但需手动在 beforeEach 中清理关键状态。 **fixtures 和 cy.task() 生成数据怎么选?** fixtures 适合不变的测试输入(表单数据、API 响应模板);cy.task() 适合需要与后端交互的动态数据(创建测试用户、插入数据库记录),task 在 Node 环境执行,能直连数据库。 **如何保证并行执行时测试数据不冲突?** 用唯一标识生成数据:`Date.now()` 或 `Cypress._.random()`,避免固定用户名。测试结束在 afterEach 中通过 cy.request 或 cy.task 清理自己创建的数据,不依赖全局 reset。 ## 写段代码 ```javascript // cy.session 加速登录 + fixtures 管理数据 Cypress.Commands.add('login', (role) => { cy.session(role, () => { cy.fixture('users').then((u) => { cy.request('POST', '/api/login', u[role]); }); }); }); ```
服务端5月29日 00:24
Cypress 和 Selenium 有什么核心区别?Cypress 直接运行在浏览器内部,通过 Chrome DevTools API 与页面通信,无需 WebDriver 中间层;Selenium 通过外部 WebDriver 进程以 HTTP 协议控制浏览器,架构上多了一跳延迟。Cypress 自动重试断言、内置时间旅行调试、仅支持 Chromium 系浏览器;Selenium 支持所有主流浏览器但需手动处理显式等待。选择依据:前端 SPA 项目选 Cypress 快速反馈,跨浏览器兼容测试选 Selenium。 ## 追问 **Cypress 的自动等待机制和 Selenium 的显式等待有什么区别?** Cypress 在断言失败时自动重试(默认 4 秒),无需手动写 wait;Selenium 必须用 WebDriverWait 显式等待元素出现,否则直接抛异常。 **Cypress 为什么不支持跨域和多标签页?** Cypress 运行在同源策略下,跨域需 cy.origin() 处理,多标签页通过模拟而非真正打开新窗口,这是架构上的硬限制。 **Selenium Grid 和 Cypress Cloud 的并行策略有何不同?** Selenium Grid 自建节点分发测试到不同浏览器,免费可控;Cypress Cloud 依赖官方服务按机器数并行,免费版有限制。 **两者在 CI/CD 中如何选择?** 小团队前端项目用 Cypress 开发体验好、上手快;大型项目需 Firefox/Safari 兼容性验证时,Selenium 更合适,也可混合使用。 ## 写段代码 ```javascript // Cypress: 自动等待,无需显式 wait cy.get('#username').type('user'); cy.get('#password').type('pass'); cy.get('button').click(); cy.url().should('include', '/dashboard'); // Selenium: 必须显式等待 WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.ID, 'username')) ); ```
服务端5月29日 00:24
Cypress 视觉回归测试怎么做?Cypress 本身不内置视觉回归功能,需借助插件实现:`cypress-image-diff`(轻量免费)、Percy/Chromatic(云端对比平台,付费)。核心流程是首次运行生成基准截图,后续运行做像素级 diff,差异超过阈值则判定失败。关键配置是 threshold 容忍度和动态内容排除策略。 ## 追问 **cypress-image-diff 和 Percy 怎么选?** cypress-image-diff 本地运行、零费用、适合小团队,截图存仓库;Percy 提供云端可视化审阅、多浏览器快照、PR 集成审批流,适合中大型团队。Percy 还能自动处理抗锯齿和字体渲染差异。 **动态内容(日期、轮播图)导致误报怎么处理?** 三种策略:1) 截图前用 CSS 隐藏动态区域 `cy.get('.carousel').invoke('css', 'visibility', 'hidden')`;2) 插件的 ignore 区域配置;3) 用 `cy.clock()` 冻结时间,使时间戳固定。 **threshold 阈值怎么设定?** 像素级对比用 0.01-0.05(严格),感知对比用 0.1-0.2(宽松)。建议核心页面 0.01,次要页面 0.1。首次跑测试建立 baseline 后再微调。 **CI 环境中截图不一致怎么解决?** CI 和本地渲染差异(字体、GPU)导致误报。方案:1) Docker 统一运行环境;2) 只在 CI 中做视觉测试;3) 用 Percy 等云端工具消除本地差异;4) 禁用动画和字体反锯齿。 ## 写段代码 ```javascript // cypress-image-diff 基本用法 cy.compareSnapshot('homepage', 0.02); // 排除动态区域 cy.get('.live-data').invoke('css', 'visibility', 'hidden'); cy.compareSnapshot('dashboard', 0.05); ```
服务端5月29日 00:23
Cypress 要不要用 Page Object Model?Cypress 官方并不推荐传统 POM——自定义命令和 App Actions 模式比 POM 更契合 Cypress 的命令链机制。POM 把 DOM 选择器封进类方法,但 Cypress 的重试机制使得类方法中返回 `this` 的链式调用容易丢失重试上下文。真正需要时,可用轻量页面对象仅封装选择器常量,操作逻辑仍交给自定义命令。 ## 追问 **Cypress 官方推荐的替代模式是什么?** App Actions:通过 `cy.request()` 直接调用 API 设置状态,跳过 UI 操作步骤。例如登录不再走页面填写表单,而是 `cy.request('POST', '/api/login', credentials)` 配合 `cy.session()` 缓存。 **POM 在什么场景下仍有价值?** 页面交互极其复杂、选择器频繁变更的 SPA 项目中,POM 的选择器集中管理仍有意义。但应避免在 POM 方法中使用 Cypress 命令,改为返回选择器字符串供测试中组合。 **POM 方法中 cy.get() 返回 this 为什么有问题?** cy.get() 返回 Chainable 而非页面对象实例,在 POM 方法中 `return this` 会导致后续 `.should()` 等断言脱离 Cypress 重试队列。正确做法是方法内完成全部操作,不返回 this 继续链式调用。 **选择器管理有没有更好的方案?** 用 `data-cy` 或 `data-testid` 属性统一选择器策略,配合自定义命令封装常用操作,比 POM 类更轻量且不丢失重试能力。 ## 写段代码 ```javascript // 推荐:选择器常量 + 自定义命令 const selectors = { email: '[data-cy=email]', submit: '[data-cy=submit]' }; Cypress.Commands.add('login', (email, pwd) => { cy.get(selectors.email).type(email); cy.get(selectors.submit).click(); }); ```
服务端5月29日 00:23
Cypress 怎么拦截和模拟网络请求?用 `cy.intercept()` 拦截匹配规则的 HTTP 请求,配合 `.as()` 别名和 `cy.wait('@alias')` 实现请求等待与断言。intercept 可返回固定 stub 响应、动态构造响应、修改请求头或延迟响应,让测试脱离真实 API 依赖。注意 intercept 必须在请求发起前注册,否则无法捕获。 ## 追问 **cy.intercept() 和已废弃的 cy.route() 有什么区别?** route 只能拦截 XMLHttpRequest,intercept 同时支持 XHR 和 Fetch API。intercept 使用 RouteMatcher 对象匹配请求(支持 method、url、headers 等多维度),功能远超 route。Cypress 6+ 已弃用 route。 **怎么 stub 一个带动态参数的请求?** 用函数式 handler:`cy.intercept('GET', '/api/users*', (req) => { req.reply({ body: mockData[req.query.page] }); })`,根据 req.query 或 req.body 动态构造响应。 **如何模拟网络错误和超时?** `cy.intercept('GET', '/api/data', { forceNetworkError: true })` 强制网络错误;`cy.intercept('GET', '/api/data', { delay: 3000, statusCode: 200, body: {} })` 模拟超时或慢响应。 **多个 intercept 匹配同一请求时哪个生效?** 按注册顺序,最后注册的优先。建议用精确匹配规则(url + method + headers)避免冲突,或用 `.as()` 明确指定等待哪个。 ## 写段代码 ```javascript cy.intercept('GET', '/api/users*', (req) => { req.reply({ statusCode: 200, body: { users: [] } }); }).as('getUsers'); cy.visit('/users'); cy.wait('@getUsers').its('response.statusCode').should('eq', 200); ```
服务端5月29日 00:23
Cypress 自定义命令怎么创建和复用?通过 `Cypress.Commands.add()` 在 `cypress/support/commands.js` 中注册自定义命令,将重复操作封装为可链式调用的 `cy.xxx()` 方法。自定义命令返回 `cy` 对象,与内置命令行为一致,支持重试和超时机制。定义时注意命名唯一、避免与内置命令冲突,复杂逻辑优先用普通工具函数而非自定义命令。 ## 追问 **Cypress.Commands.add() 的第二个参数支持哪些选项?** 可传入配置对象 `{ prevSubject: 'element' }` 使命令接收前一个命令的 subject,实现类似 `cy.get('input').myCommand()` 的链式用法。prevSubject 可选 'optional'、'required'、'noop' 等。 **自定义命令和普通工具函数怎么选?** 需要重试、超时、命令日志或链式调用时用自定义命令;纯数据处理、复杂条件逻辑用普通函数。过度使用自定义命令会导致命令日志噪音和调试困难。 **如何覆盖已有命令?** 用 `Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })` 扩展内置命令行为,如自动添加认证 header。慎用,会全局影响。 **自定义命令中如何正确处理异步?** 命令内部必须使用 Cypress 命令(cy.get、cy.request 等)而非原生 Promise,否则无法重试。如需返回值,用 `.then()` 提取。 ## 写段代码 ```javascript // cypress/support/commands.js Cypress.Commands.add('login', (email, password) => { cy.session([email, password], () => { cy.request('POST', '/api/login', { email, password }); }); }); // 测试中使用 cy.login('user@test.com', 'pass123'); ```
服务端5月28日 02:56
Cypress 如何处理动态内容等待?cy.wait() 与自动重试的最佳实践在 Cypress 测试中,动态内容(AJAX 请求、异步渲染、第三方 API)是最常见的测试不稳定来源。核心解法是两个机制:`cy.wait()` 精确等待网络请求,以及 Cypress 内置的重试能力(retry-ability)。下面逐一说明。 ## cy.wait():精确等待网络请求 `cy.wait()` 的正确用途是等待已拦截的网络请求完成,而非硬编码等待时间。 ### 基本用法 ```javascript // 先拦截,再触发,最后等待 cy.intercept('POST', '/api/login').as('loginReq'); cy.get('#login-btn').click(); cy.wait('@loginReq'); // 等到该请求完成才继续 ``` ### 关键参数 - `timeout`:超时时间,默认 5000ms,可按场景调整 - `response`:可直接断言响应内容 ```javascript cy.wait('@loginReq', { timeout: 8000 }) .its('response.statusCode') .should('eq', 200); ``` ### 等待多个请求 ```javascript cy.intercept('GET', '/api/user').as('userReq'); cy.intercept('GET', '/api/profile').as('profileReq'); cy.visit('/dashboard'); cy.wait(['@userReq', '@profileReq']); ``` ### 常见错误 - 用 `cy.wait(3000)` 硬编码等待——这是反模式,应改为等待具体请求或元素状态 - 别名未定义就 `cy.wait('@xxx')`——会直接报错 - 在 `cy.wait()` 内嵌套其他命令——会导致执行顺序混乱 ## 重试能力:Cypress 的核心设计 Cypress 的重试机制和很多人理解的不一样。它不是"失败后重试 3 次",而是查询类命令在超时时间内持续重试直到断言通过。 ### 工作原理 `cy.get()`、`cy.contains()`、`.should()` 等查询命令会不断重新查询 DOM,直到找到匹配元素或超时。这不是固定的"3 次",而是在 `defaultCommandTimeout`(默认 4000ms)内持续尝试。 ```javascript // 这行代码会在 4 秒内不断查询 #result 是否可见 cy.get('#result').should('be.visible'); ``` ### 配置超时 ```javascript // 全局配置 Cypress.config('defaultCommandTimeout', 6000); // 单条命令单独设置 cy.get('#slow-element', { timeout: 10000 }).should('exist'); ``` ### 不可重试的命令 注意,`cy.click()`、`cy.type()` 等动作类命令不会重试。如果元素还没出现就 click,会报错。正确做法是先确保元素存在: ```javascript // 错误:元素可能还没加载 cy.get('#submit').click(); // 正确:先等待元素可操作 cy.get('#submit').should('be.visible').click(); ``` ## 实战最佳实践 ### 1. 优先用 cy.intercept + cy.wait 处理异步 ```javascript cy.intercept('GET', '/api/data').as('dataReq'); cy.visit('/page'); cy.wait('@dataReq'); // 此刻数据已加载,后续断言稳定可靠 ``` ### 2. 用 should 断言代替硬等待 ```javascript // 不要这样 cy.wait(2000); cy.get('.item').should('have.length', 5); // 应该这样——Cypress 自动等待直到满足条件 cy.get('.item').should('have.length', 5); ``` ### 3. 等待加载状态消失 ```javascript cy.get('.loading-spinner').should('not.exist'); cy.get('.data-table').should('be.visible'); ``` ### 4. 测试失败自动重试配置 ```javascript // cypress.config.js module.exports = { retries: { runMode: 2, // CI 中失败重试 2 次 openMode: 0, // 本地开发不重试 }, }; ``` ## cy.wait() 与重试能力的配合 两者解决不同问题:`cy.wait()` 等待已知网络请求完成,重试能力等待未知时间的元素出现。实际项目中两者配合使用: ```javascript // 典型模式:拦截请求 → 触发操作 → 等请求完成 → 断言 UI cy.intercept('POST', '/api/submit').as('submitReq'); cy.get('#form').within(() => { cy.get('input[name="email"]').type('test@example.com'); cy.get('button[type="submit"]').click(); }); cy.wait('@submitReq').its('response.statusCode').should('eq', 200); cy.get('.success-msg').should('contain', '提交成功'); ``` 核心原则:能等请求就等请求,不能等请求就用断言让 Cypress 自动重试,永远不要用固定时间等待。
服务端5月28日 02:54
Cypress 中 cy.get() 和 cy.find() 有什么区别?Cypress 测试中,`cy.get()` 和 `cy.find()` 都能查找 DOM 元素,但行为差异很大。混用会导致测试不稳定甚至报错——比如 `cy.get('.parent').get('.child')` 看似在父元素内查找,实际上重新扫描了整个页面。本文从搜索范围、链式调用行为、性能差异三个维度讲清两者区别,并给出每个场景的选择依据。 ## cy.get() 和 cy.find() 的本质区别 核心差异只有一点:**搜索起点不同**。 - `cy.get()` 始终从文档根节点搜索,即使写在链式调用中也是如此 - `cy.find()` 从前一个命令返回的元素内部搜索,只查找后代节点 ```javascript // 看起来像在 #modal 内查找,实际不是 cy.get('#modal').get('.btn'); // .btn 从整个页面搜索,不限于 #modal 内 // 这才是只在 #modal 内查找 cy.get('#modal').find('.btn'); // .btn 仅在 #modal 的后代中搜索 ``` 这是面试中最常考的点:`cy.get()` 在链式调用中会"重置"搜索范围,而 `cy.find()` 保持在父元素作用域内。 理解这一点后,其他区别都由此派生:搜索范围不同导致性能差异,链式行为不同导致匹配精度差异,独立性不同导致使用方式差异。 ## 对比表格 | 特性 | cy.get() | cy.find() | | ------------ | ------------------------- | -------------------------- | | 搜索起点 | 文档根节点(全局) | 前一个命令的元素(局部) | | 能否独立调用 | 能,`cy.get('.item')` | 不能,必须链在前一个命令后 | | 链式行为 | 每次都从根节点重新搜索 | 在前一个元素的后代中搜索 | | 典型错误 | 链式调用时期望限定范围但未限定 | 未接父元素直接调用,抛出错误 | | 底层实现 | 等效于 `document.querySelectorAll()` | 等效于 `element.querySelectorAll()` | ## 什么时候用 cy.get() **三种典型场景:** 1. **定位页面级唯一元素**:导航栏、页面标题、主容器等。 ```javascript cy.get('nav.main-nav').should('be.visible'); cy.get('h1').should('contain', 'Dashboard'); ``` 2. **测试初始化阶段**:在 `beforeEach` 中确认页面已加载关键元素。 ```javascript beforeEach(() => { cy.visit('/login'); cy.get('form').should('exist'); // 确认表单渲染完成 }); ``` 3. **配合 .within() 限定范围后使用**:`cy.within()` 可以让 `cy.get()` 在指定容器内搜索,适合需要对同一容器内多个元素操作的场景。 ```javascript cy.get('#login-form').within(() => { cy.get('input[name="email"]').type('test@example.com'); cy.get('input[name="password"]').type('123456'); cy.get('button[type="submit"]').click(); }); ``` 注意 `cy.within()` 和 `cy.find()` 的区别:`within()` 创建一个作用域块,块内所有 `cy.get()` 都在容器内搜索;`find()` 只查找一次。如果需要对同一个父元素下的多个子元素操作,`within()` 更简洁;如果只查找一个子元素,`find()` 更直观。 ## 什么时候用 cy.find() **三种典型场景:** 1. **在已知容器内查找子元素**:表单内的输入框、列表内的特定项。 ```javascript // 验证购物车列表中的商品数量 cy.get('.cart-items').find('.cart-item').should('have.length', 3); // 查找某个表单内的提交按钮 cy.get('#registration-form').find('button[type="submit"]').click(); ``` 2. **处理重复 class 的元素**:页面上有多个 `.btn`,但只需要某个容器内的。 ```javascript // 页面有多个 .btn,只取 header 内的那个 cy.get('header').find('.btn').click(); // 对比:如果用 cy.get(),可能匹配到其他区域的 .btn cy.get('header').get('.btn'); // 搜索整个页面,可能返回错误的按钮 ``` 3. **动态渲染的列表定位**:滚动加载或异步渲染的内容。 ```javascript // 等待异步列表渲染完成后,在容器内查找最后一个元素 cy.get('.infinite-list').find('.list-item:last').scrollIntoView(); // 在动态插入的弹窗内查找关闭按钮 cy.get('.modal.show').find('.close-btn').click(); ``` ## 常见踩坑 ### 坑1:误以为 cy.get() 链式调用会限定范围 这是最常见的错误。许多开发者认为 `cy.get('.parent').get('.child')` 等价于"在 .parent 内找 .child",实际上两个 `get()` 是独立的全局搜索。 ```javascript // 错误理解:以为只在 .sidebar 内找 .active cy.get('.sidebar').get('.active'); // 实际找到页面上所有 .active // 正确做法 cy.get('.sidebar').find('.active'); // 只在 .sidebar 后代中查找 ``` 这个问题的根源在于 Cypress 的链式调用机制:`cy.get()` 总是创建一个新的查询,搜索范围重置为文档根节点。而 `cy.find()` 是在前一个查询结果的基础上继续搜索。 ### 坑2:cy.find() 不接父元素直接调用 ```javascript // 报错:cy.find() 必须接在另一个命令后面 cy.find('.item'); // TypeError: cy.find() cannot be called standalone // 正确做法 cy.get('.container').find('.item'); // 也可以用 cy.wrap() 包裹 jQuery 对象后再 find cy.wrap($element).find('.child'); ``` ### 坑3:混淆 cy.get() 的 scope 行为 在 `cy.within()` 回调中使用 `cy.get()`,搜索范围会被限定。但一旦离开 `within()` 回调,`cy.get()` 又回到全局搜索。 ```javascript cy.get('.container').within(() => { cy.get('.item'); // 只在 .container 内搜索 }); cy.get('.item'); // 离开 within 后,又变成全局搜索 ``` ### 坑4:忽视性能差异 在几十个元素的小型页面上,`cy.get()` 和 `cy.find()` 性能差距可忽略。但当 DOM 节点达到上千个时(如长列表、复杂表格),`cy.find()` 的局部搜索明显更快。实际项目中,将一个有 2000+ DOM 节点的页面测试中的全局 `cy.get()` 替换为 `cy.find()`,单次测试执行时间可以从 800ms 降到 500ms 左右。如果测试套件运行时间超过 5 分钟,建议优先检查是否有可以替换为 `cy.find()` 的 `cy.get()` 调用。 ## 与其他定位方法的配合 ### cy.contains() 结合 cy.find() `cy.contains()` 按文本内容查找元素,可以和 `cy.find()` 配合使用: ```javascript // 在特定容器内按文本查找 cy.get('.nav-menu').find('li').contains('Settings').click(); ``` ### .eq() 结合 cy.find() 当需要选择第 N 个匹配元素时,用 `.eq()` 配合 `cy.find()`: ```javascript // 选择商品列表中第二个商品的加入购物车按钮 cy.get('.product-list').find('.add-to-cart').eq(1).click(); ``` ### cy.get() + .children() vs cy.find() `.children()` 只查找直接子元素,`cy.find()` 查找所有后代: ```javascript cy.get('.container').children('.item'); // 只找直接子元素 cy.get('.container').find('.item'); // 找所有后代中的 .item ``` 根据 DOM 层级深度选择合适的方法:如果目标元素一定是直接子元素,`.children()` 语义更明确;如果层级不确定,`cy.find()` 更保险。 ## 选择决策 记住一个简单规则:**能用 `cy.find()` 就用 `cy.find()`,需要全局搜索时才用 `cy.get()`**。 - 元素在某个容器内 → `cy.get(容器).find(元素)` - 元素是页面级的 → `cy.get(元素)` - 需要在容器内连续操作多个元素 → `cy.get(容器).within(() => { cy.get(...) })` - 需要按文本内容查找 → `cy.contains(文本)` 或 `cy.get(容器).contains(文本)` 这样写出的测试代码意图更清晰,也更不容易因为页面结构变化而误匹配。在实际项目中,养成良好的元素定位习惯,不仅减少测试用例的维护成本,也能让团队其他成员更快理解测试逻辑。
服务端5月28日 02:25
Cypress 如何处理 iframe 和多窗口测试?在自动化测试中,iframe 和多窗口是两类常见的难点场景。Cypress 由于其单上下文执行架构,对这两种场景的处理方式与 Selenium 等框架有本质区别——不依赖窗口句柄切换,而是通过文档对象访问和事件监听来完成任务。理解这一设计差异,是正确编写测试用例的前提。 ## Cypress 为什么不能直接操作 iframe 内元素 Cypress 的所有命令都在主文档的上下文中执行。iframe 拥有独立的 document 和 window 对象,Cypress 的选择器无法穿透 iframe 边界。直接 `cy.get('iframe').find('button')` 会抛出元素未找到的错误,因为 `find` 只在主文档 DOM 中搜索。 这意味着你需要先拿到 iframe 的 contentDocument,再通过 `cy.wrap()` 将其纳入 Cypress 的链式调用体系。 ## 同源 iframe 的操作方法 ### 使用 its() 访问 contentDocument 这是 Cypress 官方推荐的原生方式: ```javascript // 获取 iframe 的 body 元素并操作内部内容 cy.get('#my-iframe') .its('0.contentDocument.body') .should('not.be.empty') .then(cy.wrap) .find('input[name="email"]') .type('test@example.com'); ``` 关键点: - `its('0.contentDocument.body')` 通过索引 `0` 获取第一个匹配元素的 contentDocument - `.should('not.be.empty')` 隐式等待 iframe 加载完成,避免操作未就绪的 DOM - `cy.wrap()` 将 jQuery 对象重新包装为 Cypress 可链式调用的对象 ### 封装自定义命令提高复用性 ```javascript // cypress/support/commands.js Cypress.Commands.add('getIframeBody', (selector) => { return cy.get(selector) .its('0.contentDocument.body') .should('not.be.empty') .then(cy.wrap); }); // 测试文件中使用 cy.getIframeBody('#payment-iframe') .find('input[name="card-number"]') .type('4242424242424242'); ``` 将 iframe 访问逻辑封装为自定义命令,能减少重复代码,也方便统一处理等待和错误场景。 ### 嵌套 iframe 的逐层访问 当 iframe 内还嵌套了 iframe 时,需要逐层访问: ```javascript cy.get('#outer-frame') .its('0.contentDocument.body') .should('not.be.empty') .then(cy.wrap) .find('#inner-frame') .its('0.contentDocument.body') .should('not.be.empty') .then(cy.wrap) .find('.target-element') .click(); ``` 每一层都要单独做 `.should('not.be.empty')` 断言,因为每个 iframe 的加载时机是独立的。 ## 跨域 iframe 的处理 同源策略(Same-Origin Policy)是 iframe 测试最大的障碍。当 iframe 与主页面不同源时,浏览器会阻止 JavaScript 访问 iframe 的 contentDocument,`its('0.contentDocument')` 会返回 null。 ### 使用 cy.origin() 访问跨域内容 Cypress 12+ 提供了 `cy.origin()` 命令,专门用于处理跨域场景: ```javascript describe('跨域 iframe 测试', () => { beforeEach(() => { cy.visit('https://my-app.com/page-with-cross-origin-iframe'); }); it('应能操作跨域 iframe 中的元素', () => { cy.origin('https://third-party.com', () => { cy.get('.login-button').should('be.visible').click(); cy.get('input[name="username"]').type('admin'); cy.get('input[name="password"]').type('password123'); cy.get('form').submit(); }); }); }); ``` 注意事项: - `cy.origin()` 内部无法直接引用外部作用域的变量,需要通过参数传入 - 需要在 `cypress.config.js` 中设置 `experimentalOriginDependencies: true`(Cypress 12 早期版本) - 该命令的执行上下文切换到目标域,而非操作 iframe 本身 ### 通过 cypress-iframe 插件简化操作 `cypress-iframe` 是社区维护的插件,封装了常用的 iframe 操作: ```bash npm install -D cypress-iframe ``` ```javascript // cypress/support/e2e.js import 'cypress-iframe'; // 使用插件操作 iframe cy.frameLoaded('#my-iframe'); // 等待 iframe 加载完成 cy.iframe('#my-iframe') // 获取 iframe 内容 .find('button.submit') .click(); ``` 该插件的优势在于自动处理等待逻辑,不需要手动写 `.its('0.contentDocument')` 链。但注意它只适用于同源 iframe,跨域场景仍需 `cy.origin()`。 ### 模拟 iframe 内容绕过跨域限制 当第三方 iframe 无法在测试环境中使用时,可以用 `cy.intercept()` 拦截并模拟: ```javascript cy.intercept('GET', 'https://third-party.com/widget', { statusCode: 200, body: '<html><body><div class="widget">Mocked Content</div></body></html>' }); cy.visit('/page-with-iframe'); cy.getIframeBody('#third-party-frame') .find('.widget') .should('contain', 'Mocked Content'); ``` ## Cypress 多窗口测试的变通方案 Cypress 不支持同时操作多个浏览器窗口。这是架构层面的限制——Cypress 在同一个浏览器标签页中运行,无法像 Selenium 那样通过窗口句柄切换。但这不代表无法测试涉及新窗口的场景。 ### 方案一:拦截 window.open 并在同一窗口打开 ```javascript // 在点击前拦截 window.open,改为同窗口导航 cy.window().then((win) => { cy.stub(win, 'open').callsFake((url) => { win.location.href = url; }); }); cy.get('#open-new-window-btn').click(); cy.url().should('include', '/target-page'); cy.get('.target-content').should('be.visible'); ``` 这是最常用的变通方式。将新窗口的 URL 导航到当前窗口,避免多窗口问题。 ### 方案二:提取 href 后直接访问 ```javascript // 不点击链接,而是提取 href 并直接访问 cy.get('a[target="_blank"]') .should('have.attr', 'href') .then((href) => { cy.visit(href); cy.get('.new-page-content').should('be.visible'); }); ``` ### 方案三:使用 cy.origin() 处理跨域新窗口 如果新窗口跳转到不同域名: ```javascript cy.get('a[href="https://other-domain.com/page"]').click(); cy.origin('https://other-domain.com', () => { cy.get('.page-content').should('be.visible'); }); ``` ## 常见问题排查 | 问题 | 原因 | 解决方案 | | --- | --- | --- | | `its('0.contentDocument')` 返回 null | iframe 跨域 | 使用 `cy.origin()` 或模拟 iframe 内容 | | iframe 操作间歇性失败 | iframe 异步加载未完成 | 添加 `.should('not.be.empty')` 断言等待 | | `cy.wrap()` 后命令报错 | wrap 的不是 jQuery 对象 | 确保 `.then(cy.wrap)` 而非 `.then($el => cy.wrap($el))` | | 多 iframe 定位混淆 | 选择器匹配到多个 iframe | 使用更精确的选择器如 `[src="..."]` 或 `.eq(index)` | | 新窗口测试超时 | window.open 未被拦截 | 使用 `cy.stub()` 拦截或提取 href 直接访问 | ## 追问方向 面试中回答完基础方案后,考官通常会追问以下问题: 1. **iframe 中如何处理跨域问题?** —— 重点回答 `cy.origin()` 的使用及其限制(无法引用外部变量),同时提及 `cy.intercept()` 模拟方案作为补充。 2. **为什么 Cypress 不支持多窗口?** —— Cypress 自动化工具和被测应用共享同一个浏览器窗口(通过注入脚本实现),无法同时操作多个窗口的 DOM。这是与 Selenium 的核心架构差异。 3. **嵌套 iframe 如何处理?** —— 逐层访问 contentDocument,每一层都要加断言等待加载完成。超过两层的嵌套 iframe 建议封装递归自定义命令。
服务端5月28日 01:10
Cypress 是什么?核心概念与主要特点有哪些?Cypress 是一个基于 JavaScript 的现代前端端到端(E2E)测试框架,直接在浏览器内运行测试代码,不依赖 WebDriver 等外部驱动。它由 Cypress.io 团队开发维护,以自动等待、时间旅行调试和实时重载三大特性著称,2026 年周 npm 下载量稳定在 650 万以上,仍是前端测试领域的主流选择之一。 ## 架构原理:为什么 Cypress 比 Selenium 快 Cypress 和 Selenium 的根本区别在于运行架构。Selenium 通过 WebDriver 协议在浏览器外部发送指令,每条命令都需要经过 HTTP 往返;Cypress 则将测试代码注入浏览器内部,与应用运行在同一个事件循环中,命令执行无需网络中转,官方数据显示其测试速度比 Selenium 快 2-3 倍。 | 对比项 | Cypress | Selenium | |--------|---------|----------| | 运行架构 | 浏览器内注入 | WebDriver HTTP 协议 | | 支持语言 | JavaScript/TypeScript | Java、Python、JS、C# 等 | | 自动等待 | 内置,无需手动 | 需显式等待或 Implicit Wait | | 调试方式 | 时间旅行 + 截图快照 | 截图 + 日志 | | 跨域支持 | 需配置 cy.origin() | 天然支持 | | 学习曲线 | 低,面向前端开发者 | 较高,面向 QA | 需要跨浏览器或跨语言支持时 Selenium 更灵活;专注前端项目且追求开发效率时 Cypress 优势明显。 ## 核心概念 ### 测试运行器(Test Runner) Cypress 的测试运行器直接在浏览器中执行测试代码。测试脚本与应用共享同一浏览器环境,运行器自动管理测试执行、结果报告和浏览器生命周期。测试失败时,运行器会精确定位到失败命令及对应的 DOM 快照,而非仅输出一段错误堆栈。 ### 命令链与自动等待 Cypress 通过 `cy` 全局对象提供所有测试命令,命令以链式调用组织: ```javascript cy.visit('/login') .get('#username').type('testuser') .get('#password').type('password123') .get('button[type="submit"]').click() .url().should('include', '/dashboard'); ``` 每条命令执行前,Cypress 会自动等待目标元素满足条件(可见、可交互等),无需手动添加 `sleep` 或 `waitFor`。默认超时 4 秒,可通过 `defaultCommandTimeout` 配置。这种机制大幅减少了因时序问题导致的测试不稳定(flaky test)。 ### 时间旅行(Time Travel) 这是 Cypress 最具辨识度的调试特性。测试运行器对每条命令自动生成 DOM 快照,点击任意命令即可回看该时刻的页面状态和 DOM 结构。配合 `.pause()` 断点和浏览器 DevTools,定位问题效率远高于传统截图+日志的方式。 ### 实时重载(Live Reload) 修改测试文件或应用代码后,运行器自动重新执行受影响的测试,无需手动重启。编写测试时可以边改边看结果,缩短反馈循环。 ## 关键特性 ### 网络请求控制:cy.intercept() `cy.intercept()` 是 Cypress 网络测试的核心,用于拦截、修改和模拟 HTTP 请求: ```javascript // 拦截 API 请求并返回模拟数据 cy.intercept('GET', '/api/users', { statusCode: 200, body: [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }] }).as('getUsers'); cy.visit('/users'); cy.wait('@getUsers'); // 等待请求完成 cy.get('.user-list').should('contain', 'Alice'); ``` 通过拦截网络请求,可以隔离前端逻辑与后端依赖,测试不同响应状态下的 UI 行为,也能模拟网络延迟和错误场景。 ### 跨浏览器测试 Cypress 支持 Chromium(Chrome/Edge)、Firefox 和 WebKit(Safari)家族浏览器。通过 `cypress run --browser firefox` 指定浏览器,或在 CI 中并行运行多浏览器测试。2026 年 Cypress 对 WebKit 的支持已趋于稳定,但复杂场景下仍有兼容性差异。 ### 组件测试 Cypress 9+ 引入了组件测试功能,可在隔离环境中单独测试 React、Vue、Angular 等框架的组件,无需启动完整应用。组件测试与 E2E 测试共享同一套 API,降低学习成本: ```javascript // React 组件测试示例 import { mount } from '@cypress/react'; import LoginButton from './LoginButton'; it('renders and handles click', () => { const onClick = cy.stub(); mount(<LoginButton onClick={onClick} />); cy.get('button').contains('Login').click(); expect(onClick).to.have.been.called; }); ``` ### CI/CD 集成 通过 `cypress run` 以 headless 模式执行测试,可直接嵌入 GitHub Actions、Jenkins 等流水线: ```yaml name: Cypress E2E on: [push] jobs: e2e: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: npm ci - run: npx cypress run --record --key ${{ secrets.CYPRESS_KEY }} ``` `--record` 参数将测试结果和截图上传至 Cypress Cloud,便于团队查看历史趋势和失败分析。 ## 实践要点 **安装与初始化:** ```bash npm install cypress --save-dev npx cypress open # 打开交互式测试运行器 ``` 首次运行会自动生成 `cypress/` 目录和示例测试文件。 **编写稳定测试的原则:** - 使用 `data-cy` 或 `data-testid` 属性定位元素,避免依赖 CSS 类名或文本内容 - 用 `cy.intercept()` 模拟后端响应,减少对真实 API 的依赖 - 保持测试用例独立,不依赖执行顺序 - 合理设置超时,避免全局过大超时拖慢测试 **常见踩坑:** - 跨域访问需使用 `cy.origin()`,且不能在回调中传递闭包变量 - Cypress 运行在浏览器中,无法直接测试非浏览器协议(如 WebSocket 的底层连接) - 长链式命令难以复用时,可抽取为自定义命令 `Cypress.Commands.add()` ## 局限性 Cypress 并非万能,了解其边界同样重要: - **不支持多标签页**:无法在测试中切换浏览器标签,需用 `cy.visit()` 替代 - **单浏览器上下文**:不能同时驱动多个浏览器实例进行多用户交互测试 - **跨域限制**:需显式配置 `cy.origin()`,且使用上有约束 - **不支持移动原生应用**:仅适用于 Web 应用,App 测试需配合其他工具 2026 年 Playwright 在跨浏览器和并行化方面增长迅猛,周下载量已达 3300 万,是 Cypress 的 5 倍。新项目选型时需根据团队技术栈和测试需求权衡。 ## 追问方向 - Cypress 如何处理文件上传测试? - `cy.intercept()` 的 `req.continue()` 和 `req.reply()` 有什么区别? - Cypress 的自定义命令(Custom Commands)和 Page Object Model 怎么选? - 如何在 Cypress 中实现视觉回归测试(Visual Regression)? - Cypress 与 Playwright 在 2026 年各自的优势场景是什么?
服务端5月28日 01:08
Cypress 的自动等待机制是如何工作的?Cypress 区别于 Selenium 等传统测试框架的核心能力之一,就是在执行每条命令时自动等待目标元素就绪,而不需要开发者手动插入 `wait()` 或 `sleep()`。理解这套自动等待(包括重试)机制的运行方式,是写出稳定 E2E 测试的前提。 ## 自动等待解决了什么问题 前端测试中,异步操作无处不在——DOM 渲染需要时间,网络请求需要等待响应,CSS 动画需要播放完毕。传统做法是手动加等待时间,但固定等待既浪费时间又不可靠:等短了容易 flaky,等长了拖慢整个测试套件。 Cypress 的思路是**不猜时间,而是反复检查条件**。当执行一条命令时,Cypress 会在超时窗口内持续轮询,直到目标满足条件才继续下一条命令。如果超时仍未满足,测试失败并给出清晰的错误信息。 ## 命令执行的自动等待流程 当你在测试中写下一行代码: ```javascript cy.get('#submit-btn').click(); ``` Cypress 并不会立即查找 `#submit-btn` 并点击。实际执行流程是: 1. **启动计时器**:记录当前时间戳,默认超时 4 秒(`defaultCommandTimeout`) 2. **轮询检查**:每隔约 100ms 重新查询 DOM,依次验证三个条件: - 元素存在于 DOM 中(`exists`) - 元素可见(`visible`,未被 `display:none` 或 `visibility:hidden` 隐藏) - 元素可交互(`enabled`,未被 `disabled` 属性禁用,且不在动画中) 3. **条件满足**:立即执行 `.click()` 操作,计时器销毁 4. **超时失败**:4 秒内未满足条件,抛出 `TimedOutError`,测试终止 这个流程对开发者完全透明——你只写了 `cy.get().click()`,Cypress 在内部完成了全部等待逻辑。 ## 重试机制(Retry-ability) 自动等待的核心实现是 **retry-ability**。Cypress 不仅等待元素出现,还会**重新执行整条命令链**来应对 DOM 变化。 ### 断言也会触发重试 ```javascript cy.get('.notification').should('contain', '保存成功'); ``` 这行代码中,`.should()` 断言失败时,Cypress 不会立即报错,而是回到 `cy.get('.notification')` 重新查询 DOM,再次执行断言。这个"查询 → 断言 → 失败 → 重新查询"的循环会一直持续到断言通过或超时。 这意味着:如果 `.notification` 元素还没渲染出来,或者文本还在加载中,Cypress 会自动重试,不需要你加任何额外代码。 ### 重试的范围 重试只发生在**同一个命令链**内。看这个例子: ```javascript // 这两条命令各自独立等待 cy.get('#name').type('Alice'); cy.get('#email').type('alice@example.com'); ``` `#name` 的等待和 `#email` 的等待互不影响——第一条命令完成后,才开始第二条的等待。 但如果写成链式调用: ```javascript cy.get('#form').within(() => { cy.get('input[name="name"]').type('Alice'); cy.get('input[name="email"]').type('alice@example.com'); }); ``` `within` 内部的命令共享同一个上下文,但每条命令仍然独立等待自己的目标。 ## Actionability:元素可操作性检查 Cypress 在执行交互命令(`click`、`type`、`select` 等)前,会执行严格的 actionability 检查: - **元素存在**:在 DOM 中可以找到 - **元素可见**:没有被遮挡、没有 `display:none`、`visibility:hidden`、`opacity:0` - **元素未禁用**:没有 `disabled` 属性 - **元素不在动画中**:Cypress 内置动画检测,会等待 CSS 动画或过渡完成 - **元素可滚动到视口内**:如果元素在视口外,Cypress 会自动滚动到该元素 只有全部条件通过,交互操作才会执行。这就是为什么用 Cypress 很少遇到"元素找到了但点不到"的问题。 ### 动画检测的配置 Cypress 通过 `animationDistanceThreshold` 判断元素是否还在动画中,默认值 5px。可以调整灵敏度: ```javascript // cypress.config.js module.exports = { e2e: { animationDistanceThreshold: 10, // 增大阈值,更宽松 waitForAnimations: true // 关闭设为 false } }; ``` ## 超时配置 ### 命令级超时 在单条命令上覆盖默认超时: ```javascript // 给这条命令 10 秒等待时间 cy.get('#slow-element', { timeout: 10000 }).click(); ``` ### 全局默认超时 修改所有命令的默认超时时间: ```javascript // cypress.config.js module.exports = { e2e: { defaultCommandTimeout: 8000 // 全局默认 8 秒 } }; ``` ### 不同命令的默认超时 Cypress 中不同类型的命令有不同的默认超时值: | 命令类型 | 默认超时 | 配置项 | |---------|---------|-------| | DOM 查询命令 | 4 秒 | `defaultCommandTimeout` | | 页面加载(`cy.visit`) | 60 秒 | `pageLoadTimeout` | | 网络请求(`cy.request`) | 5 秒 | `requestTimeout` | | 文件读取(`cy.readFile`) | 1 秒 | `fileServerFolder` 相关 | ## 什么时候需要手动等待 自动等待覆盖了大部分场景,但有些情况仍需显式处理: ### 等待网络请求完成 ```javascript // 用 cy.intercept + cy.wait 等待特定 API 响应 cy.intercept('POST', '/api/login').as('loginReq'); cy.get('#submit').click(); cy.wait('@loginReq'); // 等待请求完成,比等元素更可靠 ``` ### 等待非 DOM 的条件 ```javascript // 等待某个 JavaScript 变量变化 cy.waitUntil(() => cy.window().then(win => win.appLoaded === true)); ``` ### 避免的错误做法 ```javascript // 错误:用固定时间等待 cy.wait(5000); cy.get('.result').should('be.visible'); // 正确:让 Cypress 自动等待 cy.get('.result', { timeout: 10000 }).should('be.visible'); ``` `cy.wait(固定毫秒)` 是反模式——它不验证任何条件,只是盲目等待,既可能等不够也可能浪费时间。 ## 自动等待 vs 显式等待:对比总结 | 维度 | Cypress 自动等待 | Selenium 显式等待 | |-----|----------------|------------------| | 默认行为 | 所有命令自动等待 | 需要手动配置 WebDriverWait | | 重试机制 | 内置 retry-ability | 需要自己写重试逻辑 | | 断言集成 | 断言失败自动重试查询 | 断言与等待分离 | | 动画检测 | 内置 | 无 | | 超时配置 | 每条命令可单独配置 | 全局或每个等待单独配置 | ## 常见问题与排查 ### 元素明明存在却超时 通常是 actionability 检查未通过——元素被其他元素遮挡、有 `pointer-events:none`、或仍在动画中。用 `.debug()` 查看详细信息: ```javascript cy.get('#my-btn').debug().click(); ``` ### 父元素变化导致查询失效 Cypress 的重试会重新执行查询,但如果 DOM 大面积重绘,之前的元素引用可能失效。解决方案是让查询更稳定: ```javascript // 不稳定:依赖元素顺序 cy.get('li').eq(2).click(); // 更稳定:用 data 属性定位 cy.get('[data-cy="third-item"]').click(); ``` ### 条件测试的陷阱 自动等待的前提是"你知道元素会出现"。如果要测试"元素不应该出现",不能用自动等待: ```javascript // 错误:Cypress 会等待元素出现,超时才通过,浪费 4 秒 cy.get('.error-msg').should('not.exist'); // 正确:先确认元素不存在,再断言 cy.get('body').should('not.contain', '.error-msg'); ``` Cypress 从底层设计了自动等待与重试机制,让测试代码更简洁、更稳定。理解这套机制的边界——哪些场景自动处理,哪些需要显式等待——是写出高质量 E2E 测试的关键。
服务端5月28日 00:27
Cypress 中的断言有哪些类型和用法?Cypress 的断言是验证页面元素状态、属性或行为是否符合预期的核心机制。Cypress 断言基于 Chai 断言库,支持隐式断言和显式断言两种方式,并内置自动重试机制——断言失败时 Cypress 会自动重试直到超时,无需手动添加 `cy.wait()`。 ## 隐式断言与显式断言的区别 | 对比项 | 隐式断言(Implicit) | 显式断言(Explicit) | |--------|---------------------|---------------------| | 语法 | `.should()` / `.and()` | `expect()` / `assert` | | 重试 | 自动重试直到通过或超时 | 不自动重试,立即判定 | | 适用场景 | DOM 元素验证 | API 响应、复杂逻辑判断 | | 链式调用 | 支持 `.and()` 链接多个断言 | 需在 `.then()` 回调中使用 | 实际开发中,**优先使用隐式断言**,因为自动重试能大幅减少因异步渲染导致的测试不稳定问题。 ## 隐式断言:should() 与 and() `.should()` 是 Cypress 最常用的断言方法,配合链式调用 `.and()` 可以对同一元素连续验证多个条件。 ```javascript // 链式断言:验证按钮可见且可点击 cy.get('#submit-btn') .should('be.visible') .and('not.be.disabled'); // 链式文本断言 cy.get('.menu-wrapper') .should('contain', '首页') .and('contain', '关于我们'); ``` `.and()` 是 `.should()` 的别名,仅用于提升可读性,两者功能完全一致。 ## 常见断言类型 ### 1. 存在性与可见性断言 验证元素是否存在于 DOM 以及是否对用户可见,这是最基础也是最常用的断言类别。 ```javascript // 元素存在于 DOM(不要求可见) cy.get('#app-container').should('exist'); // 元素不存在 cy.get('#loading-spinner').should('not.exist'); // 元素在视口内可见 cy.get('.success-message').should('be.visible'); // 元素隐藏 cy.get('.hidden-tip').should('not.be.visible'); ``` `exist` 检查 DOM 节点存在性,`be.visible` 检查元素是否实际可见(非 `display:none`、`visibility:hidden`、宽高为 0 等)。两者区别是常见面试考点。 ### 2. 值断言 验证输入框的值、文本内容或元素数量。 ```javascript // 输入框的 value 属性 cy.get('#username').should('have.value', 'admin'); // 元素的文本内容(精确匹配) cy.get('.title').should('have.text', '欢迎使用'); // 文本包含(模糊匹配) cy.get('.status').should('contain', '成功'); // 元素数量 cy.get('.list-item').should('have.length', 5); ``` `have.text` 是精确匹配,`contain` 是包含匹配——这是另一个高频考点。 ### 3. 属性与 CSS 断言 验证 HTML 属性值和 CSS 样式。 ```javascript // href 属性 cy.get('a.home-link').should('have.attr', 'href', '/home'); // class 属性 cy.get('#tab-1').should('have.class', 'active'); // CSS 属性 cy.get('.warning').should('have.css', 'color', 'rgb(255, 0, 0)'); // data-* 自定义属性 cy.get('[data-testid="modal"]').should('have.attr', 'data-testid', 'modal'); ``` CSS 断言中颜色值需要用 `rgb()` 格式,不能直接用十六进制。 ### 4. 状态断言 验证表单元素的交互状态。 ```javascript // 禁用状态 cy.get('#submit-btn').should('be.disabled'); // 选中状态(复选框/单选框) cy.get('#agree-checkbox').should('be.checked'); // 聚焦状态 cy.get('#search-input').should('be.focused'); ``` ## 显式断言:expect 与 assert 当需要在 `.then()` 回调中对非 DOM 对象(如 API 响应、计算结果)进行断言时,使用显式断言。 ```javascript // expect 风格(BDD) cy.request('/api/user/1').then((response) => { expect(response.status).to.eq(200); expect(response.body).to.have.property('name', '张三'); expect(response.body.roles).to.include('admin'); }); // assert 风格(TDD) cy.request('/api/stats').then((response) => { assert.equal(response.body.total, 100, '总数应为 100'); assert.isArray(response.body.items, 'items 应为数组'); }); ``` 显式断言**不会自动重试**,如果 API 响应需要等待,应使用 `.its()` 配合 `.should()` 替代: ```javascript // 推荐写法:隐式断言 + 自动重试 cy.request('/api/status').its('body.status').should('eq', 'ready'); ``` ## 深度相等与对象断言 验证复杂对象或数组时,需要使用深度比较。 ```javascript // 深度相等 cy.request('/api/config').its('body').should('deep.eq', { theme: 'dark', lang: 'zh-CN' }); // 对象属性 cy.wrap({ name: 'test', age: 25 }).should('have.property', 'age', 25); // 数组长度与内容 cy.get('.tag').should('have.length', 3) .and('contain.text', '前端'); ``` ## 断言自动重试机制 Cypress 隐式断言的自动重试是区别于其他测试框架的核心特性。当断言条件不满足时,Cypress 不会立即失败,而是在超时时间内反复重试。 ```javascript // 以下断言会持续重试,直到元素可见或超时(默认 4 秒) cy.get('.notification').should('be.visible'); ``` 这意味着你**不需要**在断言前手动添加 `cy.wait()`: ```javascript // 错误写法:硬编码等待 cy.wait(3000); cy.get('.notification').should('be.visible'); // 正确写法:依赖自动重试 cy.get('.notification').should('be.visible'); ``` 如果默认超时不够,可以在命令或全局配置中调整: ```javascript // 单条命令设置超时 cy.get('.slow-element', { timeout: 10000 }).should('be.visible'); // cypress.config.js 全局配置 module.exports = { defaultCommandTimeout: 10000 }; ``` ## 常见断言速查表 | 断言 | 用法 | 说明 | |------|------|------| | `exist` | `.should('exist')` | DOM 中存在 | | `be.visible` | `.should('be.visible')` | 元素可见 | | `have.value` | `.should('have.value', 'x')` | 输入框值匹配 | | `have.text` | `.should('have.text', 'x')` | 文本精确匹配 | | `contain` | `.should('contain', 'x')` | 文本包含 | | `have.length` | `.should('have.length', n)` | 元素数量 | | `have.attr` | `.should('have.attr', 'href', '/x')` | 属性匹配 | | `have.class` | `.should('have.class', 'active')` | CSS 类匹配 | | `be.disabled` | `.should('be.disabled')` | 元素禁用 | | `be.checked` | `.should('be.checked')` | 复选框选中 | | `deep.eq` | `.should('deep.eq', obj)` | 深度相等 | 掌握 Cypress 断言的关键在于三点:优先用隐式断言获取自动重试能力,区分 `have.text` 与 `contain` 的精确/模糊匹配,以及避免在显式断言中处理需要等待的异步逻辑。
服务端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()` 显式等待。