单测覆盖不错,为什么上线前还是慌
很多前端团队会遇到一种有点尴尬的状态:单元测试覆盖率不低,核心组件也都有测试,但每次发版前,还是得靠人工把主流程点一遍。这倒不是团队不信任测试,而是因为单测和组件测试覆盖的是“零件”,没人验证“组装起来之后”真的能跑通。

E2E 测试(端到端测试)解决的就是这个问题:站在用户视角,把整个应用从头到尾走一遍。它不关心函数返回值对不对,也不关心组件渲染细节,只关心一条完整链路是否真的可用。
很多人对 E2E 的第一反应是“慢”。这话不算错,E2E 确实慢,而且不是一般地慢。但它慢得有价值——它能抓到单测发现不了的问题:接口字段对不上、路由守卫挡了不该挡的人、第三方登录跳转后丢参数、缓存把旧数据留在了页面上。这些问题在单测环境里很难构造,但在真实用户路径上就是实打实的线上事故。
既然 E2E 有不可替代的价值,那接下来真正的问题就变成了:用什么工具写?
为什么说架构差异是 Playwright 与 Cypress 选型的核心
这几年 Playwright 和 Cypress 基本是这个问题的唯二答案。两者都足够成熟,社区活跃,也都在持续迭代。选型不是“哪个更好”,而是“哪个更符合你的约束条件”。要回答这个问题,得先从架构聊起。
Cypress 的架构和大多数测试框架不一样。测试代码运行在浏览器内,和应用页面共享同一个运行环境。这种架构带来一个好处:调试体验非常自然。Cypress 的调试工具可以暂停、回溯、查看每一步的 DOM 快照,像在浏览器里做一次带录像的真人操作。
但这个架构也带来约束。因为 Cypress 的应用代码跑在页面里,它天然不好处理多标签页场景,跨页面跳转后上下文容易丢失。虽然 Cypress 提供了 cy.origin() 来处理跨域场景,但如果你要验证的是一个多标签页、多窗口的复杂业务流程,Cypress 写起来会比较别扭。
Playwright 走的是另一条路线。它通过 CDP(Chrome DevTools Protocol)驱动浏览器,测试代码跑在 Node.js 进程里,和页面不在同一个上下文。这让它可以直接操作浏览器原生能力:多页面、多上下文、WebSocket、下载文件、权限控制、模拟设备……这些对复杂业务系统来说非常关键。
Playwright 的定位更像是一个浏览器自动化框架,测试只是它的一种应用方式。所以它提供了 codegen 录制工具、Trace Viewer 回放工具、多语言 SDK(JavaScript、Python、Java、.NET)……这些能力已经超出了“测试框架”的范畴。
两种架构没有绝对优劣。Cypress 的浏览器内架构让它上手快、调试爽;Playwright 的进程外架构让它覆盖面更广、更灵活。
同一个登录用例,两种写法的差别
用一个最简单的登录流程来看两者写法的差异。Cypress 是这样写的:
describe('登录流程', () => {
it('用户可以用邮箱和密码登录', () => {
cy.visit('/login')
cy.get('[data-testid="email"]').type('user@example.com')
cy.get('[data-testid="password"]').type('password123')
cy.get('[data-testid="submit"]').click()
cy.url().should('include', '/dashboard')
cy.contains('欢迎回来').should('be.visible')
})
})
Playwright 是这样写的:
test('用户可以用邮箱和密码登录', async ({ page }) => {
await page.goto('/login')
await page.getByTestId('email').fill('user@example.com')
await page.getByTestId('password').fill('password123')
await page.getByTestId('submit').click()
await expect(page).toHaveURL(/\/dashboard\//)
await expect(page.getByText('欢迎回来')).toBeVisible()
})
从表面看,两者只是 API 风格不同:Cypress 是链式调用,Playwright 是 async/await。但真正重要的是底层的等待机制。
Cypress 的几乎所有命令都内置了自动重试,.should() 断言会在一定时间内反复检查条件,直到通过或超时。你不需要手动写 sleep 或 waitFor,只需要用正确的断言方式。
Playwright 的自动等待机制叫可操作性检查(actionability checks):在点击、输入之前,框架会检查元素是否可见、是否稳定、是否能接收事件,如果不满足就自动等待,直到超时。这同样省掉了大量显式等待代码。
这个区别在日常写测试时感受不大,但在排查 flaky 测试时非常关键。理解你的测试框架在什么条件下重试、什么时候放弃,是写稳定 E2E 测试的前提。
从选型角度做一个横向对比
下面这张表整理了我在实际选型中会比较关注的维度:
| 对比维度 | Cypress | Playwright |
|---|---|---|
| 架构 | 浏览器内运行 | 进程外通过 CDP 驱动 |
| 多标签页支持 | 弱,需要 workaround | 原生支持,多上下文 |
| 网络拦截 | cy.intercept(),功能完善 | page.route(),支持 WebSocket |
| 并行执行 | 多机器并行需要商业版 | 内置 worker 并行,免费 |
| 调试体验 | 时间旅行、DOM 快照 | Trace Viewer、codegen |
| 多语言支持 | JavaScript/TypeScript | JS/TS、Python、Java、.NET |
| 浏览器覆盖 | Chrome 系为主,Firefox/WebKit 实验性 | Chromium、Firefox、WebKit 全线支持 |
| 学习曲线 | 相对平缓 | 略陡,但生态工具更全 |
| 报告能力 | Dashboard 收费,开源报告有限 | 内置 HTML report,开源可定制 |
这张表不是让你照着选,而是帮你识别自己最在意哪个维度。
如果团队已经有比较扎实的单元测试和组件测试,只是想快速补一条回归防线,Cypress 的上手体验是最好的。文档清晰,调试直观,写测试的人不需要理解太多底层机制,开箱即用。
如果系统业务流程比较复杂,涉及多角色、多页面、支付回调、第三方跳转、消息推送这类场景,Playwright 的进程外架构会省掉很多麻烦。你可以在一个测试里同时打开多个页面,用不同角色上下文做交互,这些都是 Cypress 目前实现起来比较别扭的。
还有一点值得注意:Playwright 的多语言支持对非纯前端团队有实际意义。如果测试维护者是测试开发工程师,主要写 Python,那 Playwright 会明显降低协作成本。Cypress 在这个维度上基本锁死在 JavaScript/TypeScript 生态。
写 E2E 测试的三个常见误区
工具选好只是第一步,实际写测试的过程中,有三个误区很容易把团队带偏。
- 把 Cypress 的断言当成同步代码。Cypress 命令是队列式的,链式调用不会立即执行,.should() 自带重试。如果你写 const text = cy.get(‘.foo’).text() 这种代码,拿到的不是文本,而是链式引用。理解命令队列和重试机制,是用好 Cypress 的前提。
- 依赖 CSS 选择器定位元素。很多测试刚开始用 .btn-primary 这类类名定位,一旦样式重构或 UI 库升级,测试就挂了。正确做法是使用语义化定位,getByRole、getByLabel、getByText,或者统一约定 data-testid。这不止是 Playwright 的建议,Cypress 的 cy.contains() 和 cy.get(‘[data-testid=…]’) 同样适用。
- 追求 E2E 覆盖率。E2E 测试的定位是守住关键路径,不是测完所有分支。一次 E2E 跑下来可能就要几分钟,全量用例超过几百条时,CI 耗时和稳定性都会出问题。合理的做法是分层测试:单元测试覆盖逻辑分支,组件测试覆盖交互细节,E2E 只覆盖用户最核心的几条路径。
从零落地:先守住最不能挂的路径
如果团队决定开始做 E2E 测试,建议先做两件事。第一,挑出应用里最核心的用户旅程,比如注册登录、创建项目、发起支付、查看报表,把这些主流程写成 E2E。不需要一开始就覆盖所有边界,先守住最不能挂的路线。第二,把 E2E 接入 CI,但先作为可选门禁,跑挂不阻塞发版,只给提示,等稳定之后再变成强校验。
测试数据管理上,一定要避免共享状态。每个用例独立准备自己的数据,用完清理。否则两个用例同时跑的时候互相污染,flaky 测试就来了。对于有后端依赖的流程,用接口 mock 或者独立测试账号都可以,关键是保持每个用例的独立性。
- 先挑 3~5 条最核心的用户旅程,别一上来就铺全量。
- E2E 接入 CI 后先作为可选门禁,稳定后再变成强校验。
- 每个用例独立准备测试数据,避免共享状态。
还有一个很实际的建议:别指望 E2E 测试能替代人工回归。它能把重复劳动省掉,但视觉还原、交互手感、真实网络环境下的性能表现,这些维度 E2E 依然覆盖不到。把 E2E 当作发版流程里的最后一道自动检查,而不是唯一的质量保障,心态就对了。
回到最初的问题:为什么前端需要 E2E 测试?因为单测和组件测试回答的是“零件是否合格”,E2E 回答的是“这台机器是否真的能跑起来”。两者不是替代关系,而是不同层级的质量保障。
至于 Playwright 和 Cypress 选哪个,取决于系统复杂度、团队背景和调试偏好。如果只是快速补齐回归测试,Cypress 足够好;如果要面对复杂业务流程,Playwright 的架构上限更高。选型没有标准答案,但搞清楚自己最在意什么,答案就不难找。
原创文章,作者:fudengji,如若转载,请注明出处:https://fudengji.cn/article/481/