
1. 项目概述为什么选择Cypress如果你正在为Web应用寻找一个靠谱的自动化测试工具并且厌倦了Selenium那套需要额外驱动、复杂配置和异步等待的繁琐流程那么Cypress很可能就是你的“梦中情测”。我最初接触Cypress是因为一个紧急的回归测试任务当时用传统的工具一个简单的登录流程测试光是处理各种元素等待和弹窗就写了半屏的Thread.sleep和WebDriverWait调试时间比写代码还长。而Cypress的出现彻底改变了这种局面。简单来说Cypress是一个基于JavaScript的前端自动化测试框架。它最大的特点是运行在浏览器内部而不是像Selenium那样通过远程协议如WebDriver与浏览器通信。这个架构上的根本差异带来了颠覆性的体验测试代码和你的应用运行在同一个事件循环里这意味着Cypress能直接访问和控制应用的一切包括网络请求、DOM元素、甚至本地存储。你不再需要处理那些恼人的异步等待问题因为Cypress内置了自动重试和智能等待机制。写一个测试用例感觉就像在写同步代码一样流畅。这个项目适合谁呢如果你是前端开发者想为自己的组件或页面快速增加测试覆盖Cypress的学习曲线非常平缓它的API设计对前端开发者极其友好。如果你是专业的测试工程师希望提升UI自动化测试的稳定性和开发效率Cypress的实时重载、时间旅行调试和视频录制功能能让你快速定位问题。即便你是个自动化测试新手Cypress详尽的文档和活跃的社区也能让你快速上手。接下来我们就从零开始一步步搭建并深入一个完整的Cypress自动化测试项目。2. 环境准备与项目初始化2.1 核心依赖安装Node.js与包管理器Cypress是一个Node.js应用所以第一步是确保你的开发环境安装了Node.js。我个人推荐使用Node.js 18 LTS或更高版本它在稳定性和性能上都有很好的保证。你可以去Node.js官网下载安装包但我更推荐使用版本管理工具比如nvmWindows用户可以用nvm-windows。这样你可以轻松地在不同项目间切换Node版本。安装好Node.js后包管理器的选择也很关键。虽然Node自带了npm但近年来yarn和pnpm在速度和磁盘空间利用上表现更优。对于新项目我强烈建议使用pnpm它的安装速度极快并且通过硬链接节省大量磁盘空间。你可以通过npm install -g pnpm来全局安装它。注意如果你所在公司的内网环境有严格的代理或安全策略可能需要配置.npmrc文件来设置镜像源或代理否则后续安装Cypress二进制包时可能会因网络问题失败。一个常见的做法是使用淘宝镜像registryhttps://registry.npmmirror.com/。2.2 创建项目与安装Cypress假设我们要为一个名为“TodoMVC”的示例应用编写测试。首先创建一个新的项目目录并初始化。mkdir cypress-todo-test cd cypress-todo-test pnpm init -y这会在目录下生成一个package.json文件。接下来安装Cypress作为开发依赖。这里有个小技巧虽然你可以直接pnpm add cypress --save-dev但我建议指定一个主要版本以避免未来大版本升级可能带来的不兼容问题。目前Cypress 13.x是稳定版本。pnpm add cypress13 --save-dev安装过程会下载Cypress的二进制文件这可能需要一些时间取决于你的网络环境。安装完成后你的package.json的devDependencies里应该能看到Cypress。2.3 Cypress项目结构初探Cypress推崇“开箱即用”的理念。最快捷的启动方式是使用它的交互式打开命令npx cypress open第一次运行时Cypress会帮你初始化一个完整的项目结构并弹出一个图形化界面Test Runner。同时它会在你的项目根目录下创建一个cypress文件夹其核心结构如下cypress/ ├── e2e/ # 存放你的端到端测试用例文件.cy.js ├── fixtures/ # 存放静态测试数据如.json文件 ├── support/ │ ├── commands.js # 自定义命令用于封装复用逻辑 │ └── e2e.js # 测试运行前的全局配置和导入 └── downloads/ # 测试运行时下载的文件默认不存在会自动创建 └── screenshots/ # 测试失败时的截图默认不存在会自动创建 └── videos/ # 测试运行的录屏如果开启这个结构非常清晰。e2e目录是我们编写测试用例的主战场。fixtures用于管理测试数据实现数据与脚本的分离。support/commands.js是你施展“魔法”的地方可以把复杂的操作封装成一句简单的命令极大提升代码的可读性和复用性。3. 编写第一个端到端测试用例3.1 理解Cypress的测试语法Cypress底层基于Mocha测试框架和Chai断言库所以它的语法对于有JavaScript测试经验的开发者来说非常亲切。一个最基本的测试文件结构如下// cypress/e2e/todo_app.cy.js describe(TodoMVC 应用, () { // 测试套件描述一组相关测试 beforeEach(() { // 每个测试用例运行前的钩子函数 // 通常在这里访问被测页面例如 cy.visit(/) }); it(应该能成功添加一个新的待办事项, () { // 单个测试用例 // 测试步骤和断言 }); it(应该能将一个事项标记为完成, () { // 另一个测试用例 }); });describe和it来自Mochacy则是Cypress提供的全局命令对象所有与浏览器交互的操作都通过它来完成。3.2 实战为TodoMVC编写完整测试流让我们为一个经典的TodoMVC应用假设运行在http://localhost:3000编写测试。这个测试流将覆盖核心功能。// cypress/e2e/todo_app.cy.js describe(TodoMVC, () { beforeEach(() { // 在每个测试开始前访问应用首页并清空可能存在的本地存储数据 cy.visit(http://localhost:3000); cy.clearLocalStorage(); // 确保测试环境干净 }); it(页面加载成功并显示正确的标题和输入框, () { // 断言页面标题包含“TodoMVC” cy.title().should(include, TodoMVC); // 断言找到class为.new-todo的输入框并且它是可见的、可编辑的、且为空 cy.get(.new-todo) .should(be.visible) .and(have.attr, placeholder, What needs to be done?) .and(have.value, ); }); it(可以添加一个新的待办事项, () { const newItem 学习Cypress自动化测试; // 操作在输入框中键入文本并按下回车 cy.get(.new-todo).type(${newItem}{enter}); // 断言列表中出现新的条目且文本内容正确初始状态为未完成 cy.get(.todo-list li) // 获取列表项 .should(have.length, 1) // 断言列表长度为1 .first() // 取第一个也是唯一一个 .find(label) // 找到里面的label标签 .should(have.text, newItem); // 断言文本内容 // 断言输入框被清空准备接收下一个输入 cy.get(.new-todo).should(have.value, ); }); it(可以标记一个事项为完成状态, () { // 先添加一个事项 cy.get(.new-todo).type(完成第一个测试{enter}); // 操作点击事项左侧的复选框通常是一个.toggle类元素 cy.get(.todo-list li .toggle).check(); // 断言该事项的父级li元素应获得‘completed’类表示已完成 cy.get(.todo-list li).should(have.class, completed); }); it(可以过滤查看不同状态的事项, () { // 准备数据添加三个事项并完成其中一个 cy.get(.new-todo).type(事项一{enter}); cy.get(.new-todo).type(事项二{enter}); cy.get(.new-todo).type(事项三{enter}); cy.get(.todo-list li:nth-child(2) .toggle).check(); // 完成第二个事项 // 操作点击“Active”过滤器 cy.contains(Active).click(); // 断言只看到两个未完成的事项 cy.get(.todo-list li).should(have.length, 2).and(not.have.class, completed); // 操作点击“Completed”过滤器 cy.contains(Completed).click(); // 断言只看到一个已完成的事项 cy.get(.todo-list li).should(have.length, 1).and(have.class, completed); }); });这个测试文件覆盖了从页面加载、元素交互到状态断言的全过程。你可以通过npx cypress open打开Test Runner点击这个文件来运行它并实时观察浏览器的每一步操作。3.3 配置与自定义cypress.config.js项目根目录下的cypress.config.js是Cypress的核心配置文件。默认生成的可能很简单但我们可以根据项目需求进行深度定制。// cypress.config.js const { defineConfig } require(cypress); module.exports defineConfig({ e2e: { // 设置基础URL这样在测试中可以用 cy.visit(/) 代替完整路径 baseUrl: http://localhost:3000, // 默认超时时间毫秒 defaultCommandTimeout: 10000, // 命令执行超时 pageLoadTimeout: 60000, // 页面加载超时 // 测试文件匹配模式 specPattern: cypress/e2e/**/*.cy.{js,jsx,ts,tsx}, // 是否支持实验性的“组件测试”用于测试Vue/React组件 experimentalStudio: false, // 录制测试功能可按需开启 // 全局设置在每个测试文件加载前运行 setupNodeEvents(on, config) { // 可以在这里集成插件例如读取环境变量、生成报告等 // 例如读取不同环境的环境变量 const environment config.env.environment || development; const envConfig require(./cypress/env/${environment}.json); config.env { ...config.env, ...envConfig }; return config; }, }, // 视口设置 viewportWidth: 1280, viewportHeight: 720, // 是否录制测试视频通常CI环境下开启本地调试可关闭以提升速度 video: false, // 截图设置 screenshotOnRunFailure: true, });通过配置文件我们可以实现环境隔离开发、测试、生产、自定义超时、集成外部插件等高级功能。setupNodeEvents钩子非常强大允许你修改Cypress的内部行为。4. 进阶技巧与最佳实践4.1 使用Fixtures管理测试数据硬编码的测试数据不利于维护。当测试数据变得复杂或需要多组数据时应该使用fixtures。例如我们可以创建一个待办事项列表的数据文件。// cypress/fixtures/todoItems.json { items: [ {text: 购买 groceries, completed: false}, {text: 学习 Cypress, completed: true}, {text: 写测试报告, completed: false} ] }然后在测试中加载并使用它// cypress/e2e/todo_with_fixture.cy.js describe(使用Fixture数据的Todo测试, () { beforeEach(() { cy.visit(/); // 加载fixture数据 cy.fixture(todoItems).as(todoData); // 将数据别名化方便后续使用 }); it(批量添加fixture中的待办事项, function() { // 使用function以便访问this const items this.todoData.items; items.forEach(item { cy.get(.new-todo).type(${item.text}{enter}); if (item.completed) { // 如果是已完成状态则勾选它 cy.get(.todo-list li).last().find(.toggle).check(); } }); // 断言总数 cy.get(.todo-list li).should(have.length, items.length); }); });使用cy.fixture()不仅使数据与代码分离还支持JSON、JS、TXT等多种格式非常灵活。4.2 创建自定义命令提升复用性当你发现某些操作序列在多个测试中重复出现时就该考虑将它们封装成自定义命令了。这能极大提升代码的简洁性和可维护性。// cypress/support/commands.js Cypress.Commands.add(createDefaultTodos, () { const TODO_ITEM_ONE 学习Cypress const TODO_ITEM_TWO 编写测试用例 const TODO_ITEM_THREE 重构代码 cy.get(.new-todo) .type(${TODO_ITEM_ONE}{enter}) .type(${TODO_ITEM_TWO}{enter}) .type(${TODO_ITEM_THREE}{enter}) }); Cypress.Commands.add(addTodo, (text) { cy.get(.new-todo).type(${text}{enter}); }); Cypress.Commands.add(completeTodo, (index 0) { cy.get(.todo-list li).eq(index).find(.toggle).check(); }); // 带查询选项的命令 Cypress.Commands.add(findTodoByText, (text, options {}) { return cy.get(.todo-list li, options).contains(text); });定义好之后就可以在任意测试文件中像使用内置命令一样使用它们// 在测试文件中 describe(使用自定义命令, () { beforeEach(() { cy.visit(/); cy.createDefaultTodos(); // 一行命令完成数据准备 }); it(使用自定义命令完成一个事项, () { cy.completeTodo(1); // 完成第二个事项 cy.findTodoByText(编写测试用例).parent(li).should(have.class, completed); }); });自定义命令是Cypress项目规模化后的基石务必花时间设计好它们。4.3 拦截与存根网络请求XHR/Fetch现代前端应用大量依赖API。Cypress提供了强大的网络控制能力可以拦截、存根Stub或监听Spy网络请求实现不依赖后端服务的独立测试。// cypress/e2e/todo_api.cy.js describe(TodoMVC API 交互测试, () { it(拦截获取待办事项列表的GET请求, () { // 在访问页面前先定义要拦截的请求和返回的模拟数据 cy.intercept(GET, /api/todos, { statusCode: 200, body: [ { id: 1, text: 模拟数据1, completed: false }, { id: 2, text: 模拟数据2, completed: true } ] }).as(getTodos); // 给这个拦截起个别名 cy.visit(/); // 访问页面页面会发起GET /api/todos请求 // 等待这个特定的请求完成 cy.wait(getTodos); // 断言页面应该渲染出我们模拟的数据 cy.get(.todo-list li).should(have.length, 2); cy.contains(.todo-list li, 模拟数据1).should(exist); }); it(存根创建待办事项的POST请求, () { const newTodoText 通过API创建; // 拦截POST请求并强制返回我们指定的响应 cy.intercept(POST, /api/todos, { statusCode: 201, body: { id: 999, text: newTodoText, completed: false } }).as(createTodo); cy.visit(/); cy.get(.new-todo).type(${newTodoText}{enter}); // 等待拦截的请求发生并断言请求体 cy.wait(createTodo).its(request.body).should(deep.equal, { text: newTodoText, completed: false }); // 注意由于我们存根了响应页面显示的是我们模拟返回的数据(id:999) cy.contains(.todo-list li, newTodoText).should(exist); }); });使用cy.intercept()你可以测试边缘情况模拟服务器返回500错误测试前端错误处理。加速测试避免等待真实网络延迟直接返回预设数据。实现测试隔离确保测试结果不依赖于不稳定或未准备好的后端服务。4.4 页面对象模式Page Object Pattern随着测试套件增长直接在被测页面上使用cy.get(‘.some-class’)会导致代码高度耦合且难以维护。页面对象模式将页面元素和操作封装成类是解决这一问题的经典设计模式。// cypress/support/pages/TodoPage.js class TodoPage { // 元素定位器 elements { newTodoInput: () cy.get(.new-todo), todoList: () cy.get(.todo-list), todoListItem: () cy.get(.todo-list li), todoToggle: (index) cy.get(.todo-list li).eq(index).find(.toggle), filterAll: () cy.contains(All), filterActive: () cy.contains(Active), filterCompleted: () cy.contains(Completed), clearCompletedButton: () cy.get(.clear-completed) }; // 页面操作 visit() { cy.visit(/); return this; // 支持链式调用 } addTodo(text) { this.elements.newTodoInput().type(${text}{enter}); return this; } toggleTodo(index) { this.elements.todoToggle(index).check(); return this; } filterByActive() { this.elements.filterActive().click(); return this; } // 断言方法 shouldShowTodoCount(count) { this.elements.todoListItem().should(have.length, count); return this; } shouldShowTodoTextAtIndex(index, text) { this.elements.todoListItem().eq(index).find(label).should(have.text, text); return this; } } export default new TodoPage(); // 导出一个单例实例在测试文件中使用这个页面对象// cypress/e2e/todo_with_page_object.cy.js import TodoPage from ../support/pages/TodoPage; describe(使用页面对象的Todo测试, () { beforeEach(() { TodoPage.visit(); }); it(使用页面对象添加和验证待办事项, () { TodoPage .addTodo(页面对象模式真清晰) .addTodo(代码复用性高) .shouldShowTodoCount(2) .shouldShowTodoTextAtIndex(0, 页面对象模式真清晰) .toggleTodo(0) .filterByActive() .shouldShowTodoCount(1); }); });页面对象模式让测试脚本更接近自然语言提升了可读性。当页面UI发生变化时你只需要在一个地方Page Object更新元素定位器所有测试用例都会自动生效维护成本大大降低。5. 集成与持续测试5.1 在CI/CD中运行Cypress测试以GitHub Actions为例自动化测试的价值在于持续反馈。将Cypress集成到CI/CD流水线中可以在每次代码提交或合并时自动运行测试确保质量。下面是一个基本的GitHub Actions工作流配置示例它会在每次推送到主分支或发起拉取请求时启动一个容器安装依赖运行Cypress测试。# .github/workflows/cypress-tests.yml name: Cypress E2E Tests on: push: branches: [ main ] pull_request: branches: [ main ] jobs: cypress-run: runs-on: ubuntu-latest # 如果测试需要后端服务可以在这里启动 # services: # your-app: # image: your-app-image:latest # ports: # - 3000:3000 steps: - name: Checkout code uses: actions/checkoutv4 - name: Setup Node.js uses: actions/setup-nodev4 with: node-version: 18 cache: pnpm # 如果使用pnpm - name: Install dependencies run: pnpm install # 或 npm ci - name: Run Cypress tests # 使用Cypress官方提供的GitHub Action它封装了浏览器安装、测试运行等复杂步骤 uses: cypress-io/github-actionv6 with: # 在运行测试前可能需要先启动你的开发服务器 start: pnpm run dev # 假设你的package.json里有 dev 脚本启动应用 wait-on: http://localhost:3000 # 等待应用启动 # 指定浏览器默认为Electron browser: chrome # 是否录制视频 record: false # 并行运行测试需要Cypress Cloud账号 parallel: false env: # 传递环境变量给Cypress CYPRESS_baseUrl: http://localhost:3000这个工作流做了几件事1) 准备Node环境2) 安装项目依赖3) 使用cypress-io/github-action这个官方Action来运行测试它内部会处理Cypress二进制文件的安装和缓存4) 通过start和wait-on参数确保在运行测试前你的Web应用已经启动。5.2 生成并解读测试报告在CI环境中我们通常以“无头”模式cypress run运行测试看不到图形界面。这时清晰的测试报告就至关重要。Cypress本身会输出详细的命令行日志。此外我们可以集成第三方报告器生成更美观的HTML报告。一个流行的选择是mochawesome报告器。首先安装依赖pnpm add --save-dev mochawesome mochawesome-merge mochawesome-report-generator然后修改cypress.config.js配置reporter// cypress.config.js module.exports defineConfig({ e2e: { // ... 其他配置 reporter: mochawesome, reporterOptions: { reportDir: cypress/reports/mocha, // 报告生成目录 overwrite: false, // 是否覆盖旧的报告 html: true, // 生成html报告 json: true, // 生成json报告用于合并 timestamp: mmddyyyy_HHMMss // 报告文件名加时间戳 }, }, });运行测试时Cypress会在指定目录生成JSON格式的原始报告。如果测试文件被分割运行例如并行测试会产生多个JSON文件需要用mochawesome-merge合并再用marge生成最终的HTML。为了方便可以在package.json中添加脚本{ scripts: { test:e2e: cypress run, report:merge: mochawesome-merge cypress/reports/mocha/*.json cypress/reports/mocha/combinedReport.json, report:generate: marge cypress/reports/mocha/combinedReport.json --reportDir cypress/reports/html --inline, test:full: pnpm test:e2e pnpm report:merge pnpm report:generate } }运行pnpm test:full后会在cypress/reports/html目录下生成一个包含图表、通过率、失败详情、甚至测试步骤截图的精美HTML报告非常适合在CI流水线结束后归档或通过邮件分享给团队。6. 常见问题与排查技巧实录即使Cypress设计得再友好在实际项目中你依然会遇到各种“坑”。下面是我在多个项目中总结的一些典型问题及其解决方案。6.1 元素定位与异步等待问题问题描述测试失败报错“Timed out retrying after 4000ms: Expected to find element:.some-button, but never found it.”原因分析这是Cypress新手最常见的问题。虽然Cypress内置了自动重试和等待机制但它只对cy.get()和cy.contains()等命令作用。如果你的应用在元素出现前有复杂的异步操作如API调用、动画或者元素是动态渲染的可能需要更明确的等待。解决方案优先使用Cypress内置的断言进行等待Cypress命令链会自动等待直到元素满足断言。// 不推荐使用硬性等待 cy.wait(5000); // 脆弱时间可能不够或浪费 cy.get(.dynamic-element).click(); // 推荐使用.should()断言等待元素达到预期状态 cy.get(.dynamic-element, { timeout: 10000 }) // 可以设置更长的自定义超时 .should(be.visible) .and(not.be.disabled) .click();等待特定的网络请求如果元素渲染依赖于某个API调用拦截并等待该请求完成是最可靠的方式。cy.intercept(GET, /api/data).as(getData); cy.visit(/page); cy.wait(getData); // 等待数据加载完成 cy.get(.data-loaded-element).should(exist);使用cy.contains()处理动态文本对于文本内容会变化的元素cy.contains()比基于固定选择器的cy.get()更灵活。检查元素是否在Shadow DOM中现代前端框架如Web Components可能使用Shadow DOM。Cypress需要特殊配置或命令如cy.shadow()来访问其中的元素。6.2 测试隔离与状态清理问题描述测试用例之间相互影响A测试创建的数据影响了B测试的断言。原因分析Cypress默认在每个测试用例it块结束后不会刷新浏览器。如果应用状态如本地存储、Cookie、IndexedDB没有被清理就会污染后续测试。解决方案在beforeEach或afterEach钩子中清理状态beforeEach(() { // 清理本地存储、Cookie、会话存储 cy.clearLocalStorage(); cy.clearCookies(); cy.window().then((win) { win.sessionStorage.clear(); }); // 如果使用IndexedDB可能需要调用应用自身的重置函数 // cy.window().its(app.resetDatabase).invoke(reset); });使用独立的测试数据通过API或UI操作在每个测试开始前创建唯一的数据例如使用Date.now()或Cypress._.random(0, 1e6)生成唯一ID确保测试间不会冲突。考虑使用cy.session()Cypress 12.0这是一个实验性但非常强大的命令可以缓存和恢复整个浏览器上下文包括Cookies、本地存储等特别适合需要登录的测试能大幅提速。但需注意其使用场景和限制。6.3 跨域与iframe处理问题描述测试需要访问不同子域下的页面或者与iframe内的内容交互遇到安全策略限制。原因分析浏览器的同源策略限制了脚本与不同源页面内容的交互。Cypress做了一些特殊处理但并非所有限制都能绕过。解决方案对于二级域或端口不同的情况在cypress.config.js中设置chromeWebSecurity: false可以禁用Chrome的Web安全策略但需谨慎这降低了测试环境的安全性。module.exports defineConfig({ e2e: { chromeWebSecurity: false, }, });与iframe内容交互Cypress无法直接对iframe内的元素使用命令。你必须先获取iframe的contentWindow。// 假设iframe的id是myIframe cy.get(#myIframe) .its(0.contentDocument.body) // 获取iframe的body元素 .should(not.be.empty) .then(cy.wrap) // 将获取到的DOM元素“包装”成Cypress可操作的对象 .find(button inside iframe) // 现在可以在iframe内查找元素了 .click();这个过程比较繁琐如果可能最好的策略是让开发人员为测试提供便利例如在非生产环境中暂时禁用iframe或提供测试钩子。6.4 测试稳定性与Flaky Tests问题描述测试时而过时而失败没有稳定的重现规律。原因分析Flaky Tests是自动化测试的“毒瘤”。可能原因包括网络波动、动画未完成、第三方服务不稳定、测试数据竞争条件、时间依赖如setTimeout等。解决方案增加断言减少依赖不要假设操作完成后状态会立刻改变。在关键操作后添加明确的、针对最终状态的断言。避免使用cy.wait(毫秒数)硬性等待是Flaky Test的根源之一。用等待元素、请求或应用状态来代替。使用重试机制Cypress Test Runner和cypress run命令都支持对失败测试进行重试。可以在配置或测试级别开启。// 在cypress.config.js中全局配置 module.exports defineConfig({ retries: { runMode: 2, // 在cypress run模式下失败后重试2次 openMode: 0, // 在cypress open交互模式下不重试 }, }); // 或在describe/it块中局部配置 describe(不稳定的功能, { retries: 3 }, () { it(某个测试, () { ... }); });隔离第三方依赖使用cy.intercept()存掉所有对外部API的调用让测试在一个完全可控的环境下运行。定期维护测试用例将Flaky Tests纳入技术债务定期审查和修复。可以借助Cypress Dashboard的“Flaky Test”识别功能商业功能。6.5 调试技巧时间旅行与实时快照Cypress Test Runner最强大的功能之一就是“时间旅行”调试。当测试在交互模式下运行时你可以悬停在命令日志的任意一步上右侧的浏览器会精确回退到该命令执行后的状态。实操技巧.pause()命令在测试脚本中插入cy.pause()测试运行到此处会暂停你可以打开浏览器开发者工具检查此时的DOM、网络、控制台然后再继续执行。.debug()命令插入cy.debug()Cypress会在此处暂停并将当前 yield 的对象通常是上一个命令的结果打印到控制台。你可以检查它的属性。利用cy.log()和cy.task()cy.log()可以在命令日志中输出自定义信息。对于更复杂的Node端调试可以使用cy.task()从测试代码向setupNodeEvents中的插件逻辑传递消息并打印。查看失败快照测试失败时Cypress会自动在失败前的那一步截取快照。点击命令日志中的失败步骤可以仔细查看当时的UI状态这比看静态的错误信息有用得多。从零搭建一个Cypress项目远不止是安装和写几个测试用例。它涉及到项目结构设计、测试策略制定、持续集成、以及应对实际复杂场景的诸多技巧。我个人的体会是前期在自定义命令、页面对象和网络请求拦截上多投入一些时间设计后期维护成本会呈指数级下降。不要追求一次性写出完美的测试而是采用迭代的方式先从核心业务流程开始让测试跑起来再逐步重构、扩展和优化。当你的测试套件能够稳定、快速地在每次提交时提供反馈你会真切感受到自动化测试带来的信心和效率提升。最后一个小建议把Cypress的测试也当成产品代码来对待进行代码审查这能有效保证测试代码的质量和可持续性。