首页 / 资讯中心 / 文章详情

端到端测试实战:Playwright搭建稳定e2e环境与用例设计

端到端测试实战:Playwright搭建稳定e2e环境与用例设计 ★ FEATURED ARTICLE
1. 先把 e2e 的价值和边界理清楚聊到自动化测试绕不开的一个词就是 e2e。我第一次认真接触 e2e是在一次线上事故之后单测覆盖率接近 98%接口测试也全绿结果用户从商品页加购到提交订单时前端漏传了一个字段整条链路挂掉等用户投诉我们才发现。单测告诉我“每个零件没问题”接口测试告诉我“相邻零件能接上”但用户不关心零件用户只关心整台机器能不能跑起来。e2e 要回答的就是这个问题。e2e 全称是 End-to-End Testing端到端测试核心是模拟真实用户的操作路径。它不是验证某个函数返回值对不对也不是验证某个接口返回的 JSON 是否有缺字段而是从浏览器或应用入口开始像用户一样点按钮、填表单、跳页面最终看业务结果是否符合预期。这篇文章适合前端、后端、测试工程师也适合一个人扛一个项目的全栈开发者我会把选型、用例设计、环境隔离和常见坑都讲一遍。1.1 e2e 到底在测什么很多人会把 e2e 当作“高级一点的 UI 测试”这个理解不准确。UI 测试关注的是界面元素长什么样、能不能点而 e2e 关注的是业务链路通不通。我习惯把自动化测试分层看测试层次回答的问题典型工具单元测试这个函数、这个类的逻辑对吗Jest、Vitest、JUnit接口测试服务端参数校验、契约和幂等性对吗Postman、Superteste2e 测试用户按真实路径操作时整套系统通不通Playwright、Cypress、Selenium这里的关键差异在“真实路径”。比如用户注册完能不能直接登录加了购物车之后价格是否一路带到结算页支付回调之后订单状态是否能在订单列表显示。这些跨模块、跨系统、跨前端和后端的流程单测覆盖不到接口测试只能覆盖到单个服务只有 e2e 能从头到尾验证一遍。1.2 不是所有项目都需要满仓 e2e先泼一盆冷水e2e 不是万灵药也不是覆盖率越高越好。e2e 的成本比单测和接口测试高一个量级因为要启动真实浏览器、准备真实页面、维护测试账号和数据一条用例跑下来秒级起步整个套件跑完可能就要十几分钟甚至更久。适合上 e2e 的项目有几个特征核心业务链路一旦出问题影响面很大比如登录、下单、支付、导出这类路径。系统涉及多个端或多个服务前后端交互复杂单靠接口测试拼不出完整画面。产品处于相对稳定阶段页面结构和交互不会每周大改否则你一直在修测试而不是写业务。团队有持续维护自动化用例的意愿而不是写出来就再也不管。如果你正在做一个两天就能上线的营销页或者页面原型还在频繁推倒重来我建议先别上 e2e。等关键路径稳定了再补核心流程也不迟。我现在的做法是用 e2e 守住主干链路用单测和接口测试守住细节逻辑用代码评审和人为验证覆盖边角。2. 从零搭一套能落地的 e2e 环境工具选型这步说难不难说简单也容易踩坑。市面上主流方案就那几个关键是你知道自己需要什么。我的经验是先看三点调试体验、自动等待能力、多浏览器支持。这三个直接决定你日常维护测试代码的心情。2.1 框架选型Playwright、Cypress、Selenium 怎么选我最早用的是 Selenium那时候写一条用例要自己处理元素等待一会儿sleep一会儿轮询稍微复杂点的流程就跑得提心吊胆。后来项目里试过 Cypress调试体验确实舒服但在多标签页和跨域场景上有些局限。最近这一两年我主力用的是 Playwright原因是它在设计上把你从“写等待逻辑”这件烦心事里解放了出来。对比维度SeleniumCypressPlaywright支持的浏览器Chrome、Firefox、Edge、Safari 等主要是 Chrome 系跨浏览器支持弱一些Chromium、Firefox、WebKit 全支持等待机制基本靠手动处理显式等待要自己写自动重试断言体验不错内置 Web-first 断言自动等待可点击、可见调试能力依赖第三方工具和日志浏览器内可直接看运行过程Trace Viewer、录视频、截图、UI 模式用例稳定性元素定位方式多样但太看重写法比较稳定社区教程多稳定性和可维护性都比较优秀适用场景老项目兼容、跨语言团队前端团队、偏单页应用全栈团队、复杂业务链路我个人很推荐如果你还在纠结我给一个最朴素的建议新项目直接选 Playwright它是当前自动化测试体验和工程化能力平衡得最好的方案。如果团队已经有成熟的 Selenium 资产也没有必要为了“追新”推倒重来先解决稳定性问题更实际。2.2 用 e2e profile1 隔离测试运行环境这里我想展开一个很多人会忽略的细节浏览器 profile 的隔离。你可能在项目里见过类似e2e profile1的命名我第一次看到时还以为是个配置文件名后来才明白它指的是给自动化测试单独准备的一套浏览器用户数据目录。手动打开浏览器时浏览器默认会用一个本地 profile 保存登录态、Cookie、LocalStorage 和历史记录。如果自动化测试也直接复用这个默认 profile很容易出问题测试脚本里的账号登录状态可能残留到你日常浏览里反过来你日常登录的其他站点也可能影响测试结果。更麻烦的是如果测试用例之间共用同一个 profile前一个用例留下的状态会污染后一个用例。正确做法是给 e2e 单独开一个 profile比如就叫profile1。在 Playwright 里可以使用持久化上下文import { chromium } from playwright; // 指定独立的用户数据目录运行数据和日常浏览器完全隔离 const context await chromium.launchPersistentContext(./profile1, { headless: false, viewport: { width: 1280, height: 720 }, }); const page await context.newPage(); // 首次登录后Cookie、LocalStorage 会保存在 ./profile1 目录里 await page.goto(http://localhost:3000/login); await page.getByLabel(手机号).fill(e2e_account); await page.getByLabel(密码).fill(test123); await page.getByRole(button, { name: 登录 }).click();这套做法的好处是测试账号和日常账号不会串。多个测试账号可以用profile1、profile2分开互不打扰。如果脚本运行中弹出了真实浏览器窗口也不会污染你平时的登录信息。在本地调试时第一次登录后状态可以复用后续跑用例更快。另外要提醒一句profile1这种目录属于运行时产物一定要加到.gitignore里别让一堆 Cookie 文件混进版本库。在 CI 环境里我通常不会复用 profile而是每次构建用干净目录配合后面要说的storageState来提升效率。2.3 目录结构、命令和 CI 接入e2e 用例的目录结构不宜堆成一团我现在的习惯是按业务模块划分e2e/ spec/ auth.spec.ts order.spec.ts payment.spec.ts fixtures/ user.json product.json helpers/ login.ts createOrder.ts global-setup.ts playwright.config.tsspec目录放用例文件fixtures放测试数据helpers放会被多个用例复用的公共动作比如登录、下单、造数据。全局 setup 里可以做一件事准备测试账号和基础数据。这样每个用例文件都只描述“用户视角下发生了什么”而不是把一堆底层操作重复粘贴。在本地跑测试时我的package.json脚本大概是这样{ scripts: { e2e: playwright test, e2e:headed: playwright test --headed, e2e:smoke: playwright test --grep smoke, e2e:report: playwright show-report } }CI 接入也很简单流水线里装依赖、启动被测服务、跑npx playwright test最后把 HTML 报告作为构建产物上传。不要只在合并代码后跑建议在每次 pull request 里跑一个精简版的冒烟用例集全量用例放到每日定时任务里这样既能快速反馈又不会让 CI 等太久。3. 实操跑通一条从登录到下单的完整链路理论讲再多不如看一条真实用例。我拿最常见的“登录到下单”链路举例这条路径覆盖了页面跳转、表单填写、按钮交互、接口响应和业务结果断言是 e2e 最典型的应用场景。3.1 第一个用例长什么样我强烈建议第一个用例不要贪多先把主干打透。比如“用户能从登录走到提交订单”这个用例就够说明 e2e 的核心写法了。import { test, expect } from playwright/test; test(用户能从登录走到提交订单, async ({ page }) { // 打开登录页 await page.goto(http://localhost:3000/login); // 用对用户可见的文案定位输入框和按钮 await page.getByLabel(手机号).fill(13800138000); await page.getByLabel(密码).fill(test-password); await page.getByRole(button, { name: 登录 }).click(); // 断言登录成功跳转到了首页 await expect(page).toHaveURL(/\/home/); // 点击第一个商品的详情再点立即购买 await page.getByRole(link, { name: 商品详情 }).first().click(); await page.getByRole(button, { name: 立即购买 }).click(); // 填写收货信息 await page.getByLabel(收货人).fill(张三); await page.getByLabel(手机号).fill(13800138000); // 提交订单 await page.getByRole(button, { name: 提交订单 }).click(); // 关键业务结果断言 await expect(page.getByText(下单成功)).toBeVisible(); });这段代码里最值得注意的是定位方式。我用的是getByLabel和getByRole这类基于用户可见语义的定位符而不是 CSS 选择器。因为前端稍微重构一下 DOM#app div form input[typetext]可能就崩了但“这个输入框的标签叫手机号”这种描述和用户看到的一致页面改版后只要你没把文案改掉用例大概率还稳得住。3.2 登录态处理每次都重登还是复用上面这条用例每次都先走登录这是最稳妥、也最接近真实用户的做法。但问题也很明显如果一套 e2e 有几百条用例每条都从登录开始光登录就要浪费大量时间。所以实践中我会分场景处理。登录流程本身一定要跑而且一定要在 e2e 套件里覆盖。但其余不负责测登录的用例可以通过storageState复用登录状态。Playwright 的storageState可以把 Cookie 和 LocalStorage 存成 JSON 文件跑用例前先执行一次登录脚本生成这个文件后面所有用例直接加载。// 先运行这段脚本把登录状态保存下来 await context.storageState({ path: e2e/.auth/user.json });然后在playwright.config.ts的 project 里配置projects: [ { name: chromium, use: { browserName: chromium, storageState: e2e/.auth/user.json, }, }, ],这样做能省掉很多重复步骤代价是登录状态可能过期。所以我在 CI 里会加一步“先生成 storageState再跑剩余用例”生成失败就直接失败避免因为登录态过期导致几百条用例同时红掉。这比每个用例自己重登要快也比把状态写到公共 profile 里要干净。3.3 等待、断言和稳一点的点击前端测试最大的敌人是时序。接口返回有快有慢动画有长有短如果测试代码里写的是waitForTimeout(3000)那这个用例就是在赌运气今天网络快3000 毫秒可能够明天接口慢了一拍用例就挂了。Playwright 的 Web-first 断言天生带自动等待比如await expect(locator).toBeVisible()会在指定时间内不断重试直到元素可见或超时。这个重试机制比我手动写等待要可靠得多因为它等待的是“业务状态”而不是“流逝的时间”。我在写用例时会遵循几条简单原则优先用toBeVisible()、toHaveURL()、toHaveText()这类断言而不是isVisible()加expect。不要用page.waitForTimeout(1000)来等页面稳定除非实在没有别的办法。点击之前优先让 Playwright 自己判断是否能点击它会自动等待元素可见、可操作。如果页面有遮罩层或 loading先把这些干扰元素处理掉再继续操作避免点击被拦截。3.4 失败现场截图、视频、trace 是救命稻草e2e 用例失败不可怕可怕的是你完全不知道它为什么失败。只看“点击按钮超时”这一行日志很难猜出是按钮没渲染还是按钮被弹窗挡住了还是页面直接白屏。所以我现在的配置里截图、视频和 trace 是默认打开的import { defineConfig } from playwright/test; export default defineConfig({ use: { baseURL: process.env.E2E_BASE_URL ?? http://localhost:3000, viewport: { width: 1280, height: 720 }, locale: zh-CN, trace: on-first-retry, screenshot: only-on-failure, video: retain-on-failure, }, // 也可以根据需求加多浏览器项目 projects: [ { name: chromium, use: { browserName: chromium } }, ], });trace: on-first-retry表示用例第一次失败时记录 trace第二次重跑时如果还失败你能在 Playwright 的 Trace Viewer 里看到每一步操作前后的 DOM 快照、网络请求和控制台日志。截图和视频看着更直观但 trace 信息量最大。排查问题的时候我一般第一件事就是打开 trace 文件看最后一步到底发生了什么。4. 常见问题与排查技巧实录e2e 写起来不算难真正难的是保持稳定。一个用例跑十次有一次失败这在 e2e 里非常常见行业里管这种现象叫 flaky test。我这里记录几个我自己踩过的高频坑。4.1 不稳定用例的五个来源现象最常见原因建议处理用例偶尔超时接口响应慢、动画未结束用自动等待断言不要硬编码 sleep点击报 intercept元素被遮罩层或弹窗挡住用角色定位符操作前处理浮层断言文本偶尔不一致接口返回了随机数据或没有 mock测试数据造固定值或用正则断言一个用例挂了连带一片用例之间共享了全局状态每个用例独立准备数据必要时串行执行本地能过、CI 挂环境差异比如字体、屏幕分辨率、接口地址不对统一baseURLCI 和本地用同一套配置我自己最深的体会是大多数 flaky 不是框架不稳定而是测试代码写得不严谨。把依赖推到“固定 sleep”上等于把稳定性交给运气。4.2 定位符的坑能省掉一半 flaky定位符是 e2e 里最影响稳定性的因素。刚学的时候我习惯打开 DevTools 右键复制 CSS 路径结果前端加了一个容器用例就挂了。后来我慢慢改成了三原则优先用getByRole、getByLabel、getByText这类语义定位符因为它们接近用户视角。实在要加>
阅读完成 · 觉得有帮助?
咨询建站