ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从体验、原理到工程落地,Cypress 让前端 E2E 测试真正可用

从体验、原理到工程落地,Cypress 让前端 E2E 测试真正可用 第一次真正意识到 Cypress 的特殊之处不是在我写完第一份端到端测试的时候而是在我看着团队里一个搁置了三个月的 Selenium 项目被重新捡起来的那一刻。那个项目没有换人没有重写业务代码只是把跑测试、看报错、定位元素这一整条链路换到了 Cypress 上。前端团队对 E2E 测试的态度也从“知道该做但不想碰”变成了“测一下也没多麻烦”。有人把 Cypress 看成又一个 E2E 框架但真正用一阵子之后会发现它解决的不是“能不能自动化”的问题而是“跑挂了之后你能不能快速知道为什么”的问题。后者才是绝大多数前端项目真正缺的东西。这篇文章我想从体验、原理、工程落地三个角度把自己用 Cypress 过程中沉淀下来的理解写清楚。1. 为什么前端端到端测试过去一直让人想跳过1.1 测试代码不是最难写的最难的是“等”和“查”如果你写过传统的浏览器自动化测试一定对这类片段不陌生先 sleep 三秒等页面加载再 sleep 两秒等接口返回然后才敢点击下一个按钮。问题在于网络请求和页面渲染的完成时间在大多数情况下不是一个固定值。sleep 短了测试在慢环境里就会不稳定sleep 长了整个测试套件跑一遍要拖成十分钟起步。这不是写测试的人偷懒而是旧架构从根上决定的测试进程和被测页面处于两个互相隔离的运行环境里。测试代码只知道“我发出去了一个指令”但无法准确知道页面那一边到底发生了什么。于是只能用固定时间来猜猜不中就换更大的时间。越复杂的页面这种等待越多测试脚本自然就越臃肿越让人不想维护。1.2 失败之后定位问题的时间比写测试还多很多团队不是没尝试过端到端测试而是写了一批用例之后发现测试经常红而且红得莫名其妙。常见画面是CI 上某个用例失败了打开日志看到一句element not interactable或者timeout waiting for element。你根本不知道是页面没加载出来还是元素选择器写错了还是上一个用户的操作污染了状态。更麻烦的是本地开发和 CI 环境不一样本地跑一遍可能是通过的CI 上却稳定复现失败。你只能到处加截图、加日志、再跑一次看结果。这个过程重复几次之后团队就会本能地开始不信任这套测试。最后的结果往往不是把测试修好而是把失败断言先注释掉或者直接砍掉整个端到端测试的 CI 任务。写测试是为了省时间最后反而成了时间黑洞。1.3 Cypress 换了一个出发点先解决反馈循环Cypress 想清楚了一件事与其让测试脚本努力去控制一个外部浏览器不如让测试运行在浏览器里和应用共享同一个运行时。这意味着 Cypress 可以实时感知页面里的 DOM 变化、网络请求、对话框、甚至 JavaScript 报错。它不需要靠 sleep 去猜页面什么时候稳定而是通过自动重试去等待应该出现的条件。更重要的是每一步测试执行完Cypress 都会在命令行面板里留下快照你可以用鼠标悬浮到每一步上去看当时页面长什么样。这套设计的本质是把“测试失败后定位问题”的成本降到了接近零。它让端到端测试不再只是交付前的验收动作而是开发过程中可以随时打开、随时调试、随时定位问题的工具。2. Cypress 的架构选择决定了它的体验上限2.1 “跑在浏览器里”不是一个实现细节而是一个产品策略Selenium 的思路是通过 WebDriver 协议从外部进程向浏览器发起命令。这个方案成熟跨语言、跨浏览器但代价是外部驱动者和浏览器内部状态之间存在一层协议隔膜。你看到的页面状态始终是“间接获得”的。Cypress 反过来测试代码直接运行在浏览器里和被测应用同一个事件循环。这意味着 Cypress 可以等待某个元素出现时直接轮询 DOM而不是通过远程命令一问一答。监听未捕获的 JavaScript 异常并且在测试失败时把异常信息直接展示出来。拦截网络请求因为所有网络请求都发生在同一个浏览器上下文中。这个差异是 Cypress 流畅体验的根源也是它某些能力边界的根源。它本质上不是一个“浏览器控制器”而是一个“应用增强器”。2.2 自动等待用“重试”代替“sleep”Cypress 里几乎所有对界面状态的断言都会默认自动重试。例如cy.get(.user-name).should(have.text, 张三)如果第一次检查时文本还不正确Cypress 会反复查询和校验直到条件满足或超时。这意味着绝大多数异步渲染场景不需要手写等待逻辑。你不再需要去想“接口还要多久才能回来”只要说清楚“最后应该是什么样子”剩下的交给 Cypress。这个设计降低了编写成本也降低了维护成本。团队里新手接手测试时不太容易出现“不知道该 sleep 多久”的困惑。2.3 网络层拦截让测试数据变成一个可控变量端到端测试最怕外部依赖不稳定后端返工、测试库被改、第三方接口超时。Cypress 的cy.intercept()允许你在浏览器层面直接拦截特定请求并且返回你指定的数据。这样一来你可以做到同一个测试用例每次跑都能返回同一份用户数据。不用后端专门为测试准备一套环境。可以模拟异常状态比如 500、超时、空数据来验证前端异常处理是否符合预期。从工程实践来看这个能力把 E2E 测试从“依赖整个系统稳定”的脆弱状态变成了“前端自己可控”的状态。它是 Cypress 用例在 CI 上能稳定运行的重要基础。2.4 时间旅行调试失败不再是一堆日志Cypress 的命令执行列表里每一步都会留下一个页面快照。执行到哪一步页面状态就是什么样子。鼠标悬停在任意一步上能看到当时页面上的输入框内容、下拉框状态、网络请求等。这个体验非常接近前端开发里的调试工具。以前测试失败后要反复“读日志猜现场”现在可以直接“回到现场看状态”。团队协作时同事抛出来一条 Cypress 失败记录你点几次鼠标就能明白问题出在用户操作序列的哪一环而不是把一堆截图发来发去。3. 先跑通一个最小可用测试从安装到断言3.1 环境准备只需要一个普通前端项目Cypress 对新手友好的一个地方在于它不要求你先搭一套复杂的测试服务。你只要有 Node.js 项目安装依赖后Cypress 可以直接打开自己的图形化运行器。npm init -y npm install cypress --save-dev如果你用 pnpm 或 yarn也可以对应地执行pnpm add cypress -D或yarn add cypress --dev。首次启动时建议先打开图形化界面它会在第一次运行时自动生成默认配置npx cypress open打开之后Cypress 会生成一套标准的项目结构。需要留意的是不同版本的默认目录有差异新版v10 之后默认把测试文件放在cypress/e2e/下旧版则是cypress/integration/。如果本地生成的结构和网上教程不一致先确认 Cypress 版本再调整路径别急着复制粘贴。3.2 编写第一个测试别急着选元素先想清楚怎么定位以最简单的登录流程为例常见写法是这样的describe(登录流程, () { beforeEach(() { cy.visit(/login) }) it(输入正确凭证后跳转到首页, () { cy.get([data-testidusername]).type(tester) cy.get([data-testidpassword]).type(123456) cy.get([data-testidsubmit]).click() cy.url().should(include, /dashboard) cy.contains(欢迎回来).should(be.visible) }) })这里最值得关注的是选择器。很多人在前面几轮写测试时最喜欢直接用类名或 text比如cy.get(.btn-primary)或cy.contains(登录)。在页面还比较简单时这确实能跑通。但一旦组件库升级、样式重构或者国际化文案调整这些选择器就会集体失效。我更建议从第一天就统一使用>cy.intercept(GET, /api/user/profile, { fixture: profile.json }).as(getProfile) cy.visit(/profile) cy.wait(getProfile) cy.get(.user-name).should(have.text, 测试用户)这样测试用的数据完全由 fixture 文件控制你可以预先准备各种边界状态的数据空列表、异常状态、超时、超大字段等。对前端团队来说这就像前端开发时已经习惯用的 mock 层只不过被 Cypress 接到了整个测试流程里。3.4 不要写一堆 if 条件判断状态刚接触 Cypress 的人容易把它当成普通编程来写比如在测试里写if (cy.get(...).should(...))或者用变量去存元素状态。这种做法通常会让 Cypress 的自动等待和重试机制失效。Cypress 的推荐做法是用链式命令和断言表达预期而不是用程序逻辑去分支。如果场景确实复杂应该考虑把多分支拆成多个独立的测试用例每个用例只覆盖一条明确路径。这样失败时定位更精准维护起来也更容易理解。4. 工程化落地单条用例跑通只是开始4.1 测试策略分级先测什么比测多少更重要一个常见误解是端到端测试覆盖率越高越好。实际落地时覆盖所有页面往往会让维护成本爆炸。更好用的分层思路是冒烟测试核心链路能否跑通。比如登录、首页加载、关键列表展示。核心链路测试用户最常走的关键路径。比如下单流程、发布流程。回归测试涉及历史功能的关键回归点。不需要追求每个按钮都覆盖。从工程经验看我会建议先只覆盖一类团队最痛、每次发版前都要手动点的那条路径。把它变成自动化用例之后团队会立刻感受到“不用手动重复点一遍”的价值然后再慢慢扩展。4.2 在 CI 里稳定运行的三要素很多团队在本地跑 Cypress 一切正常一上 CI 就各种挂。复盘下来问题大多出在三件小事上数据要可控。测试用例不应该依赖某一个开发环境里才能登录的账号。优先通过接口 mock、测试数据工厂或独立测试库来保证输入数据每次都相同。测试要相互独立。不要让用例 A 依赖用例 B 之前设置的数据。Cypress 默认会清空 cookies 和 localStorage但如果你自己维护了数据库状态或全局变量仍然会造成污染。要关心并发资源。如果是多 worker 并行跑测试容易遇到资源竞争比如同一份测试数据被多个任务同时写入。要么隔离数据要么在 CI 里减少并行度。CI 里使用cypress run时可以按自己的习惯分组执行npx cypress run --spec cypress/e2e/**/*.cy.js默认情况下Cypress 在run模式下会记录视频失败时会自动截图。不需要这些产物时可以在配置里关掉以减少 CI 时间。4.3 让测试自己告诉你在哪失败好的测试失败信息应该一眼能看懂。Cypress 给了几个能力来保证这一点给等待的请求起别名比如.as(getProfile)。用should()写断言时描述清楚预期。在关键操作前用cy.get(...).should(be.visible)做显式等待减少因为页面还在渲染导致的误报。很多人会因为怕测试太啰嗦而不写这些前置断言。但实际运维时这些断言能让测试失败时人类一秒钟定位到业务语义而不是面对一个“元素找不到”的原始状态。4.4 长期维护最怕的不是测试挂掉而是测试没人敢动端到端测试最大的风险不是初期搭建而是半年后的维护。如果测试因为选择器脆弱、数据不稳定而频繁变红大家就会习惯性忽略它然后测试慢慢失去价值。所以我会建议在项目里把“测试能不能被快速重跑、失败信息能不能快速定位”当成比“覆盖率”更重要的指标。如果某条测试经常不稳定说明它本身的数据控制或选择器设计有问题应该尽早修复而不是在失败列表里让它躺着。5. 最容易踩的坑和一套排查链路5.1 脆弱选择器是测试崩掉的第一个来源E2E 测试之所以让别人觉得“不靠谱”大部分来自选择器太脆使用动态生成的 id比如#user-123。使用样式 class比如.ant-btn-primary。使用页面文案的完全匹配比如cy.contains(立即提交)但文案变成了“重新提交”。这些选择器在页面改版时几乎必然失效。更稳定的做法是给测试专用属性或者使用相对稳定的语义定位。页面上逻辑重要、经常被测试引用的元素最好拥有唯一且稳定的>
返回列表