Cypress端到端测试实战指南:从Selenium到稳定自动化

Cypress端到端测试实战指南:从Selenium到稳定自动化 Cypress 是目前前端端到端测试领域使用率很高的开源工具由 cypress-io 组织维护。它解决的问题很具体让前端开发者和测试人员用接近自然语言的方式编写浏览器自动化测试并在测试失败时快速定位是代码问题、选择器问题还是时序问题。对于从 Selenium 迁移过来的团队Cypress 最大的不同是执行模型和调试体验测试命令直接运行在真实浏览器上下文里不依赖 WebDriver 服务失败时能看到完整页面快照、网络请求和命令执行栈。这篇文章面向还没有系统使用过 Cypress 的开发者也适合已经在写用例但经常遇到“测试不稳定”的团队。读完可以完成一个本地可运行的端到端测试项目理解 Cypress 的命令队列、重试、拦截器和数据隔离策略并拿到一份可直接用于团队排错和 CI 接入的实践清单。1. 先理解 Cypress 的测试模式再决定怎么用1.1 Cypress 与 Selenium 的差异Cypress 不是 Selenium 的替代版而是重新设计了执行架构。Selenium 通过 WebDriver 协议控制浏览器自动化脚本和浏览器是两个进程脚本通过 HTTP 接口向浏览器发送命令浏览器端做出响应。这种方式跨语言、跨浏览器能力强但存在两个问题一是调试链路长测试脚本里看不到页面内部状态二是经常需要处理“元素还没出现”的竞态所以代码里到处是显式等待。Cypress 采用另一种思路test runner 进程与浏览器通过 WebSocket 连接命令在浏览器内部执行页面应用和测试代码处于同一运行时环境。因此 Cypress 可以在命令超时时自动等待元素出现并且默认每个命令都会等待前面的命令和断言完成。这段差异直接决定了使用方式。用 Selenium 时测试用例会写很多sleep(1000)或WebDriverWait用 Cypress 时不需要在普通交互前写固定等待因为 Cypress 的cy.get()会自动重试元素查找should()断言也会在超时时间内不断评估。但“不用写等待”不等于没有等待Cypress 有默认超时时间默认是 4 秒可以在配置里修改。理解这一点非常重要否则你会把 Selenium 的习惯带进来在 Cypress 里随意cy.wait(1000)反而制造不稳定。1.2 Cypress 能覆盖哪些测试类型Cypress 的主要使用场景是端到端测试E2E也就是模拟用户从打开页面、点击、输入到看到结果的完整过程。它也可以做组件测试使用cy.mount()挂载 Vue、React 组件测试组件交互和状态。组件测试需要额外的适配器例如cypress/react、cypress/vue并且使用 Vite 或 Webpack 构建。另一个常见用法是 API 测试Cypress 可以直接调用接口并校验响应但这不是它的主打优势团队如果已经有 Postman/Newman 或专门的 API 测试工具不一定需要迁移到 Cypress。从项目阶段看Cypress 适合验证核心业务路径比如注册登录、购物流程、权限管理。它不太适合用来做大量细粒度单元测试也不适合模拟复杂的多浏览器并行行为比如同时操作多个标签页或跨域多系统跳转。对于这类场景单元测试框架、Playwright 或其他方案可能更合适。选型时要把“Cypress 适合什么”和“团队需要什么”放在一起而不是因为社区热度高就全面替换。1.3 适合与不适合的场景速查场景是否适合 Cypress说明核心业务端到端流程回归适合用例编写直观失败定位快React/Vue 组件交互测试适合需要安装对应适配器并配置构建大量纯接口测试一般可测但专业接口测试工具更成熟多标签页场景不适合Cypress 主要限制在同一标签页内需要跨域访问多个不同域部分支持可通过 cy.origin 处理但复杂度高与数据库状态强绑定的测试适合但有条件需要清理和初始化测试数据注意Cypress 不等于“只要替换 Selenium 就能解决所有 E2E 问题”。选型前先用一两个核心流程做小范围试点确认 Cypress 能在你的技术栈中稳定运行再逐步推广。2. 环境准备与项目初始化2.1 Node.js 版本和编辑器准备Cypress 是 Node.js 工具安装前需要确认本机 Node 环境。不同版本的 Cypress 对 Node 版本要求不同新版本通常要求 Node.js 18 或更高但具体以项目安装时官方文档的 engines 字段为准。可以用node -v查看当前版本。如果本机版本过旧建议用 nvm 或 fnm 管理多版本 Node避免为了一个项目全局升级导致其他项目受影响。编辑器推荐 VS Code配合 ESLint、Prettier 和 Cypress 官方 VS Code 扩展写测试时可以获得自动补全和调试支持。Cypress 的 TypeScript 类型定义是开箱可用的如果项目本身使用 TypeScript可以直接在cypress.config.ts里写配置。建议在开始前先确认这些内容Node.js 版本满足 Cypress 要求。网络源可正常安装 npm 依赖。本机具备运行图形界面或无头模式的条件。待测项目可以本地启动或有一个稳定的测试环境地址。2.2 在一个新目录里安装 Cypress推荐在独立目录里做最小实验而不是直接往现有项目里塞依赖这样可以避免依赖冲突也更容易排查问题。以cypress-e2e-demo为例mkdir cypress-e2e-demo cd cypress-e2e-demo npm init -y npm install cypress --save-dev安装过程会因为要下载浏览器二进制文件而比较慢时间取决于网络状况。安装完成后用npx cypress verify验证二进制文件是否完整。如果是第一次运行npx cypress open会创建一组示例文件和默认目录。这里要区分一下Cypress 10 之后的版本E2E 测试目录是cypress/e2e组件测试目录是cypress/component配置文件是cypress.config.jsCypress 9 及更早版本使用cypress/integration和cypress.json。网上很多文章还停留在老目录结构落地时要注意版本。2.3 配置文件的必要项在项目根目录创建cypress.config.js。以下是一个适合本地静态页面测试的基础配置const { defineConfig } require(cypress); module.exports defineConfig({ e2e: { baseUrl: http://localhost:8080, specPattern: cypress/e2e/**/*.cy.{js,jsx,ts,tsx}, supportFile: cypress/support/e2e.js, viewportWidth: 1280, viewportHeight: 720, defaultCommandTimeout: 5000, }, });配置项说明baseUrl测试中cy.visit(/)会基于该地址拼接避免每个用例写完整 URL。specPattern匹配测试文件位置和格式*.cy.js是 10 之后的常见命名。supportFile每个 spec 运行前会自动加载的公共文件适合放全局命令和注册事件。viewportWidth和viewportHeight指定浏览器视口大小影响响应式页面断言。defaultCommandTimeoutcy.get()等命令找不到元素时的默认重试超时时间单位毫秒。生产环境还会额外配置retries、video、screenshotOnRunFailure和reporter这些放到 CI 接入一节再展开。2.4 学习环境与生产环境的差异学习环境里直接npx cypress open打开交互式测试运行器可以看到左侧测试用例列表、右侧浏览器预览和运行日志。生产或持续集成环境里服务器通常没有图形界面应该使用npx cypress run执行无头测试。两种场景还需要关注数据隔离和资源释放本地可以用测试数据库或 mock 数据CI 环境最好使用独立环境或 Docker 容器避免测试数据污染生产。还要注意 Cypress 本身会保存运行视频和截图默认配置下这些文件可能很大CI 上要配置保留策略。3. 编写第一个可运行的端到端测试3.1 先做一个能本地启动的测试页面为了不依赖外部网站我们创建一个极简 HTML 页面作为被测对象。这个页面包含一个搜索输入框、一个按钮和一个结果列表脚本根据输入内容过滤内置数据。这样测试就可以真实地跑在本地不受网络波动影响。创建index.html!DOCTYPE html html langzh-CN head meta charsetUTF-8 title本地搜索页/title /head body h1测试数据搜索/h1 input idsearch-input placeholder请输入关键字 / button idsearch-btn搜索/button ul idresult-list/ul script const items [Cypress 教程, 前端测试, 端到端测试]; document.getElementById(search-btn).addEventListener(click, function () { const value document.getElementById(search-input).value.trim(); const list document.getElementById(result-list); list.innerHTML ; items .filter(function (item) { return item.indexOf(value) ! -1; }) .forEach(function (item) { const li document.createElement(li); li.className result-item; li.textContent item; list.appendChild(li); }); }); /script /body /html在项目里安装一个轻量静态服务器方便启动页面npm install http-server --save-dev修改package.json的 scripts统一入口{ scripts: { serve: http-server . -p 8080, test: cypress run, test:open: cypress open } }3.2 创建第一个 spec 文件在cypress/e2e下创建first-test.cy.jsdescribe(本地搜索页面, () { beforeEach(() { cy.visit(/); }); it(输入关键字后能过滤出匹配项, () { cy.get(#search-input).type(测试); cy.get(#search-btn).click(); cy.get(#result-list .result-item).should(have.length, 1); cy.get(#result-list .result-item).first().should(contain.text, 前端测试); }); it(输入不存在的关键字后结果列表为空, () { cy.get(#search-input).type(不存在的词); cy.get(#search-btn).click(); cy.get(#result-list .result-item).should(not.exist); }); });这里的beforeEach会在每个用例之前执行cy.visit(/)保证用例可以独立运行。cy.get(#search-input).type(测试)模拟用户输入cy.get(#search-btn).click()模拟点击最后通过should()断言列表数量和数据内容。注意should(not.exist)表示元素不存在这是 Cypress 中判断空列表的常用方式。3.3 用 Test Runner 运行和验证开启两个终端一个运行npm run serve启动静态页面另一个运行npm run test:open打开 Cypress 交互界面。在 Test Runner 中点击first-test.cy.js会看到用例在浏览器里自动执行。执行过程中每一步命令都会显示在左侧面板鼠标悬停能查看命令执行前后的页面快照。如果断言失败页面会停留在失败状态并高亮匹配的元素这样可以直接看出是选择器写错还是交互逻辑不对。3.4 用命令行方式运行和生成结果交互界面适合开发和调试CI 场景要使用命令行npm test默认会在无头浏览器中运行所有匹配的 spec 文件。运行结束后终端会显示每个测试用例的通过率、耗时、视频路径和截图路径。如果某个用例失败Cypress 会把失败瞬间的页面截图保存下来。它的路径通常位于cypress/screenshots或cypress/videos具体取决于配置。运行命令时也可以只运行指定文件npx cypress run --spec cypress/e2e/first-test.cy.js4. 核心 API 与常用断言写法4.1 常用命令的工作方式Cypress 的命令不是普通函数而是会被排进命令队列依次执行。cy.get()返回的不一定是 DOM 元素本身而是一个可以被后续命令链式调用的对象。所以不能这样写const button cy.get(#search-btn); button.click(); // 错误click 需要通过链式调用正确写法是cy.get(#search-btn).click();这也解释了为什么可以在.should()中写断言表达式却不能在普通 JS 中直接拿到元素做if判断。如果确实需要获取元素的文本或属性可以用.then()把 DOM 元素取出来。但要注意.then()内的代码不会自动重试所以在回调中做复杂条件逻辑时要谨慎。常用命令速查命令作用示例cy.visit()打开页面cy.visit(/login)cy.get()按选择器查找元素cy.get([data-cysubmit])cy.contains()按文本内容查找元素cy.contains(保存)cy.type()输入文本cy.get(input).type(admin)cy.click()点击元素cy.get(button).click()cy.should()执行断言并自动重试cy.get(.item).should(have.length, 3)cy.intercept()拦截并响应网络请求cy.intercept(GET, /api/todos)cy.fixture()读取测试数据文件cy.fixture(user.json)4.2 自动等待与重试机制Cypress 的cy.get()和cy.contains()会在元素不存在时持续重试直到超时。should()断言也会重试直到断言通过或超时。这套机制替代了 Selenium 中的显式等待是 Cypress 测试稳定的基础。因此不建议在交互动作前随意写cy.wait(1000)。固定等待会让用例变慢也会在网络波动时引发偶发失败。正确的做法是让命令去等待真实条件比如等待一个元素出现、等待一个请求完成、等待文本内容变化。// 不推荐固定等待 cy.wait(2000); // 推荐等待请求完成 cy.intercept(GET, /api/todos).as(getTodos); cy.visit(/); cy.wait(getTodos); // 推荐等待元素可见 cy.get(.loading).should(not.exist);4.3 常见断言写法Cypress 内置的 Chai 风格断言可以通过should()直接使用。以下是一些常用写法cy.get(.item).should(have.length, 3); cy.get(.item).first().should(contain.text, Cypress); cy.get(input).should(be.visible); cy.get(button).should(be.disabled); cy.url().should(include, /login); cy.request(POST, /api/login, { username: admin }).then((res) { expect(res.status).to.eq(200); });如果断言比较复杂也可以把should()的回调与expect()结合例如校验表格中的每一行。需要注意should(callback)回调会在重试期间反复执行所以不要在回调里做有副作用的操作。5. 数据、拦截器与测试隔离5.1 用 cy.intercept 接管真实的网络请求端到端测试最怕的就是“测试环境不稳定”。如果被测页面依赖后端接口后端假死、数据未初始化、网络超时都会让前端用例失败但问题其实不在前端。cy.intercept()可以在浏览器网络层拦截请求返回 mock 数据也可以只监听请求而不改变响应。cy.intercept(GET, /api/todos, { fixture: todos.json }).as(getTodos); cy.visit(/todos); cy.wait(getTodos); cy.get(.todo-item).should(have.length, 2);这里把/api/todos的返回值替换成cypress/fixtures/todos.json的内容。这样测试不依赖真实后端适合做边界场景。但在关键路径上也建议保留一部分真实后端的用例避免项目只在 mock 数据下通过、联调时立刻暴露问题。5.2 fixture 与测试数据准备fixture 文件放在cypress/fixtures下可以是 JSON、图片、CSV 等。管理 fixture 时要注意不要让不同用例共用相同数据而产生隐式依赖。建议每个功能模块使用独立的 fixture 文件并在命名中体现用途例如login.valid.json、todo.empty.json。如果项目使用真实后端还要考虑数据初始化。常见的做法是通过cy.request()调用后端提供的测试数据重置接口。在beforeEach中创建当前用例需要的用户、订单等数据在afterEach中清理。使用独立的测试数据库避免测试数据和开发数据混在一起。5.3 测试隔离策略从清 Cookie 到清数据Cypress 默认在每个测试文件开始前清除浏览器上下文但同一个 spec 内的多个用例之间不会自动清理除非你使用了实验性的隔离模式或显式清理。为了用例可独立运行推荐在beforeEach中做环境复位。比如登录状态beforeEach(() { cy.clearCookies(); cy.clearLocalStorage(); cy.visit(/); });但注意如果项目使用 localStorage 保存 tokency.clearLocalStorage()可能导致所有页面都需要重新登录这本身没问题关键是团队要对“每个用例是否需要从登录开始”达成一致。对复杂系统通常会把登录封装成自定义命令在需要时直接调用而不是每个用例都从头走一遍登录流程。6. 常见问题排查从失败用例倒推原因6.1 定位不到元素选择器问题优先现象是cy.get(#xxx)超时报错信息通常为“Timed out retrying: Expected to find element...”。处理顺序是先看页面快照确认元素是否真的已经渲染。如果元素在页面中检查选择器是否被阴影 DOM、iframe 或动态 class 影响。如果元素是异步渲染检查是否已经有网络请求完成。不要在 CSS 类选择器上投入太多稳定的期望项目重构 CSS 时测试也会跟着碎。更可靠的做法是给关键元素添加>{ scripts: { ci:e2e: start-server-and-test serve http://localhost:8080 test } }start-server-and-test会先启动服务等待端口可访问后再运行测试测试结束后自动停止服务。生产 CI 还要注意使用retries配置允许用例在失败时重试 1 次但不要依赖重试掩盖不稳定。设置video和screenshotOnRunFailure根据团队需求开启或关闭。在 GitLab CI、GitHub Actions 或 Jenkins 中将 Cypress 运行在专用的 Node 镜像或容器中。对测试结果进行归档方便团队成员回看失败截图和录屏。示例配置const { defineConfig } require(cypress); module.exports defineConfig({ e2e: { baseUrl: process.env.CYPRESS_BASE_URL || http://localhost:8080, setupNodeEvents(on, config) { // 这里可以注册插件或任务 return config; }, }, retries: { runMode: 1, openMode: 0, }, video: true, screenshotOnRunFailure: true, });7.4 可复用的落地检查清单以下清单适合团队第一次把 Cypress 从零接入到日常回归时逐项确认[ ] Node.js 版本与 Cypress 版本匹配。[ ] 测试环境地址稳定测试数据可独立初始化。[ ] 关键元素有稳定的>