
1. e2e 到底在测什么——重新理解端到端测试做开发这几年我见过太多类似的场景单测覆盖率号称 80% 以上接口测试也铺了一大片CI 全绿结果每次发版前手工点点点还是能从某个犄角旮旯里发现一个能让主流程直接断掉的 bug。产品经理一脸困惑开发小哥一脸委屈最后所有人把目光投向我这个测试工程师。这种时候我一般会反问一句你们 e2e 覆盖了吗e2e 这个词是 End-to-End 的缩写端到端测试。它和其他测试层级最本质的区别在于单测验证的是一个函数、一个方法在隔离环境下的正确性接口测试验证的是服务端逻辑在给定输入下的响应而 e2e 测试是从真实用户视角出发把整个系统当作一个完整的黑盒来验证。用户从打开页面开始输入账号密码、点击按钮、跳转页面、提交表单、看到结果这一整条链路里涉及到的前端渲染、网络请求、后端接口、数据库读写、第三方服务回调任何一个环节出问题e2e 都会如实呈现出来。很多团队对 e2e 的认知存在一个偏差觉得它就是用 Selenium 录个脚本模拟点点点而已。早几年的工具生态确实是这样但现在的 e2e 已经发展成一个完整的技术体系从用例设计、环境管理、数据隔离、并发执行、CI 集成到稳定性治理每一环都有成熟的方案。本文想做的就是把我这些年从零搭好一套 e2e 测试体系的经验完整梳理一遍包括工具选型背后的思考、用例场景的落地方式、我在真实项目里踩过的一堆坑以及e2e profile1这种场景配置到底怎么用才算合理。不管你是刚准备给项目引入 e2e 的新手还是已经跑了一段时间但总觉得用例又慢又脆的同行这篇文章都能给你一些可以直接用的东西。先说清楚一个边界问题e2e 不是什么都能接手。它解决的是整体流程是否能走通的问题而不是具体某个算法是否正确的问题。一个页面上的按钮点击之后没有反应这可能是前端事件绑定问题也可能是后端接口 500还可能是网络层发出去的请求丢了包。单测和接口测试能在各自层级帮你定位问题到底出在哪一层而 e2e 的价值在于快速告诉你有一个地方坏了至于坏在哪需要靠日志、靠链路追踪、靠分层测试去缩小范围。所以一套健康的测试体系必然是金字塔结构大量的单测做底座适量的接口测试撑中层少量但关键的 e2e 用例守顶层。层与层之间各司其职谁也不要想着替谁干活。1.1 为什么单元测试覆盖满了线上还是出问题我接手过一个电商项目后端用 Spring Boot前端用 Vue单测覆盖率报表非常好看行覆盖率和分支覆盖率都过了 75%。但每次发版进灰度总有那么几个用户能复现出让人哭笑不得的流程 bug比如用户把商品加进购物车之后直接刷新页面购物车徽标数变成 0 但购物车详情页里依然有商品比如用户在下单页切换收货地址之后运费没有跟着重新计算再比如用户在微信内置浏览器里支付成功后页面一直停在支付中的状态因为支付回调里依赖的某个参数在微信 WebView 环境下被吞掉了。这些问题有一个共同特征它们都能在浏览器里直接复现而且路径清晰、逻辑明确可单测就是测不出来。为什么因为单测测的是函数输入输出而真实用户面对的是一个有状态、有时序、有外部依赖的完整系统。上述三个 bug 分别涉及 localStorage 数据同步、组件状态跨页面传递、第三方浏览器环境的兼容处理这些交互层面的逻辑单测不是不能写而是写起来成本极高、收益极低开发同学光是要模拟一个从列表页到详情页再到购物车再回来的完整浏览器状态机就足以让人崩溃。e2e 在这个场景下反而是最经济的方案它不止测某个函数而是测整条链路在真实浏览器里的真实表现。线上频繁出问题的第二个原因是数据流的断裂。后端接口单测往往会做一堆 mock把数据库、缓存、第三方服务全部替掉保证测试隔离性这本身没错。但 mock 得越多离真实环境越远那些只有在真实环境里才会暴露的问题比如某个字段为 null 导致前端渲染异常、数据库某个字段长度不够导致插入失败、Redis 里的缓存过期策略和代码假设不一致等等单测永远模拟不出来。e2e 天然贴近真实环境它是当前所有自动化测试层级里最接近用户手测的方式也因此最有能力兜住这类问题。1.2 e2e 测试的定位与边界给项目引入了 e2e 之后还得想清楚一个特别重要的问题哪些场景值得用 e2e 来覆盖哪些场景不应该用 e2e 硬上。这个问题想不清楚测试套件很容易变成又慢又脆的累赘。我自己的原则是三个高频、关键、跨端。高频是指用户每天都会走的核心路径比如登录、搜索、浏览、加购、下单这类路径一旦挂掉影响面是全局性的值得用 e2e 天天守夜关键是指业务上不可妥协的高价值流程比如支付回调、政企客户的 OA 审批流、报表系统的导出下载这些流程容错率极低跨端则是指涉及多种技术栈协作或者多端联动的场景比如前端调 API、又依赖 WebSocket 推送的状态同步这种链路中间某一环的接口测试做得再好也无法验证端到端的表现只能靠 e2e 补位。反过来一些纯粹文案渲染、样式错位、轻量交互的问题用组件级快照测试或者人工走查可能更快更精准强行写成 e2e 反而会放大执行不稳定性的影响。另外性能测试该用专业压测工具去做安全测试该用扫描和人工渗透去做这些都有各自的专业工具链不要把 e2e 工具硬套上去。测试的终极目的不是证明自动化有多强大而是在合理成本内把风险控制住这个逻辑要时刻记住。2. 工具选型与方案设计——从零开始搭建 e2e 测试框架讲完了为什么做接下来就是怎么选和怎么搭。工具选型这件事我在不同项目里反复经历过好多次每一次都觉得这次选对了但实际操作一段时间之后总会发现一些文档里看不到的问题。这里把最主流的几个方案放在一起对比着说也把我最终的选择和理由交代清楚。2.1 主流 e2e 工具的真实对比先说一个基本情况目前业界主流的 e2e 工具无外乎 Selenium、Cypress、Playwright、Puppeteer 和 TestCafe 这几家。Selenium 是爷爷辈生态庞大支持的语言多浏览器兼容性强但它有几个让人头疼的老毛病一是依赖 WebDriver 协议启动浏览器的时候要单独起一个 driver 进程环境配置复杂度高二是内置等待机制偏弱测试里动不动就要手工 sleep导致执行既慢又不稳定三是脚本录制的体验虽然好但录完的脚本几乎都需要大量手工调试维护成本非常高。现在很多团队在用 Selenium但基本都是历史包袱新团队很少再选它做主力了。Puppeteer 是 Chrome 团队推出的用 DevTools Protocol 直接控制浏览器性能和调试体验都不错但它的定位更偏向浏览器自动化脚本而不是一个完整的测试框架。你用它做爬虫、做截图、做 PDF 生成非常顺手但要做断言、做测试报告、做失败重试它本身不带这些能力需要自己集成。TestCafe 不需要浏览器驱动内置了断言和等待机制用起来也算友好但它的社区生态明显没有前几家活跃遇到问题能搜到的解决方案相对少。真正在这几年拉开车距的是 Cypress 和 Playwright。Cypress 的定位是前端开发者的 e2e 测试工具它把开发体验做得极其流畅树形结构展示用例、自动等待机制、时间旅行回放这些特性让它在一众工具中非常有辨识度。但 Cypress 有几个先天限制我比较介意它不支持多标签页真实操作不支持同时控制多个浏览器进行跨端验证网络层控制也只能有部分手段。如果你的项目只是一个纯前端单页应用Cypress 会特别舒服但一旦涉及多 Browser Context、多用户身份切换、复杂的移动端浏览器兼容测试Cypress 就会让你在很多边缘场景里到处找 workaround。Playwright 最大的优势是它从一开始就站在完整自动化解决方案的角度来设计架构。底层协议同样是 DevTools Protocol但它把 Selenium、Puppeteer、Cypress 这些工具的常见痛点都在框架层做了处理自动等待、网络拦截、多页面多上下文管理、移动端设备模拟、文件下载、麦克风权限弹窗处理、Trace 视频回放这些能力都是开箱即用的。而且 Playwright 在浏览器支持上覆盖 Chromium、Firefox、WebKit这一点对兼容性测试尤其重要。下面这个表格是我基于实际体验整理的对比维度SeleniumCypressPlaywrightPuppeteerTestCafe安装复杂度高需单独管理 driver低npm 安装即可低会自动下载浏览器低但浏览器支持单一低无需 webdriver浏览器支持Chrome/Firefox/Safari/Edge 等仅 Chromium 族为主Chromium/Firefox/WebKit 全覆盖仅 ChromiumChromium/WebKit 等自动等待弱需大量手工同步内置体验极佳内置可自动等待元素、请求、页面加载需自己写轮询内置多标签页/多用户支持不支持原生支持多 Browser Context支持但需要手写支持有限网络控制/拦截弱部分支持原生支持可拦截请求、模拟断网支持支持有限失败诊断Screenshot内置时间旅行 DebuggerTrace Viewer 视频回放 截图需自行集成截图社区生态最庞大活跃快速增长活跃较小这些差异不是哪个更好的静态对比而是要回到你的实际项目结构里去判断。我强烈建议在做选型之前先花一个下午把自家最核心的主流程用两到三个候选工具各写一遍真实跑一轮不要只看文档里的宣传话术。2.2 我最终选择的技术栈与理由去年我负责一个中大型 B2B 交易平台的 e2e 测试体系搭建前端是 React TypeScript后端是微服务架构一共拆分了几十个服务不少业务场景需要登录不同的租户账号、切换不同的后台角色而且平台对外还要兼容主流浏览器的最新两个版本。在那一轮候选工具实测里我最终选了 Playwright 作为主力框架。选它的理由归纳下来有三条。第一是多浏览器支持和多上下文管理能力强悍。B2B 平台最先要考虑的就是不同角色打开同一页面看到的数据和菜单不一样而 Playwright 的 Browser Context 机制可以非常干净地模拟多个独立会话同时并存这个场景用两个 Context 分别模拟普通租户和管理员不需要反复登录退出效率提升特别明显。第二是它的等待机制足够聪明。它内置的 Auto-wait 机制会在执行点击、输入、查看元素等操作前自动等待元素达到可操作状态比如可见、稳定、可点、可编辑这样写出来的脚本天然稳定很多不用像老一套工具那样到处塞 sleep 或者轮询方法。第三是它的失败诊断体验太好用了。跑挂了可以直接拿到 trace 文件里面记录的是整个执行过程的网络请求、DOM 快照、控制台日志和鼠标轨迹还原现场就像看一段录播排查问题效率极高。当然我也不是没评估过 Cypress。坦白说如果你的应用是纯前端重度交互型产品用户路径高度集中在 Web 端不怎么涉及多端协同和真实性极强的浏览器兼容Cypress 的开发体验和能力其实是够用的。我在另一个内部运营系统项目里就用了 Cypress因为它够简单、够快团队里几乎所有前端同学都能快速上手这是它最大的优势。但在本文所述的这个 B2B 平台的场景里Playwright 更像那个没有明显短板的六边形战士。2.3 测试环境与数据隔离方案工具选完了紧接着要解决的就是测试运行环境和数据问题。这两件事如果不提前设计好后面不管用例写得再好都会被环境不稳定和数据污染拖垮。先说环境。e2e 测试的最佳运行环境是什么很多人第一反应是直接用线上环境测测出来就是真实结果。这个想法大错特错。线上环境的数据是不可控的你不知道什么时候会有运营人员改了一条配置、用户下了一笔大额订单更不能接受测试数据污染生产数据而且线上环境不具备随时部署最新代码的条件。我的建议是搭建一套专门的 e2e 测试环境它不要求和生产环境同等规格但它的分支来源、服务部署、第三方依赖都必须是可控的。最简单的做法就是让 CI 流水线在每次合并到主干后自动构建一套环境跑完 e2e 再销毁这样环境是干净的、代码是新鲜的可复现性也最强。不过这种方式成本高如果团队资源紧张也可以退一步用固定环境但一定要配合好数据清理策略。数据隔离是另一个大坑。我遇到过太多次这样的情况e2e 用例执行到中途页面崩溃了查了半天发现是上一个用例留下的脏数据把一个唯一键给占住了。为了避免这种互相干扰我一般会做两层处理一是每条用例执行前通过 API 快速准备自己的专属数据避免不同用例共享同一份数据二是尽可能让测试数据带上用例特征标识比如生成一个带时间戳的账号名这样即使哪条用例清理失败也不会立刻影响其他用例。这里有个额外的技巧是能用 API 预置数据的场景就绝不要通过 UI 去创建数据。比如测试下单流程你可以先用 API 把该准备的商品库存、优惠券、用户余额都DP配置好然后在 UI 层只管走下单路径这样既节省执行时间又能大幅降低因前置步骤失败导致整条用例挂掉的概率。3. 实操过程全记录——写出一套能稳定跑完的 e2e 用例到了真正动手写用例的阶段就不能只聊理念了。这一节我把一个真实项目的 e2e 落地过程完整拆开包括怎么定义场景配置、怎么写核心用例、怎么接入 CI 和产出报告每个环节都附上关键代码和参数选择逻辑。我会用一个电商风格的交易平台作为贯穿案例这样讲解起来更有实感。3.1 从 profile1 场景出发设计用例框架e2e profile1这个词不是随手搜出来的它代表的是一个实际存在的概念测试场景配置画像。在 e2e 测试里profile画像本质上是一组预先定义好的场景参数集合它告诉测试框架在什么环境、用什么身份、走哪条路径、侧重验证什么。设计 profile 的价值在于当你的业务足够复杂e2e 用例数量多到一定程度时你不可能每次都跑全部用例必须根据版本变更的影响面选择性地执行某些 profile。我在 B2B 平台项目里设计了三类 profileprofile1核心交易链路。模拟一个标准租户账号从登录开始依次完成搜索商品、发起询价、加入采购单、提交订单、线上支付的完整流程属于日常每次发版都要全量回归的守门员用例集合。profile2后台管理链路。模拟不同角色的后台操作系统包括管理员审批订单、财务导出对账单、运营配置商品上下架。这类 profile 测试的是权限控制和流程审批的完整性。profile3稳定性与异常链路。覆盖网络抖动、接口超时、服务降级等异常情况下的用户提示和降级表现比如断网后重试、服务 503 时的友好报错等。这里重点说说 profile1因为它是整个体系里最核心也最容易被写垮的部分。 profile1 的落脚点不应该是一堆零散的 UI 操作步骤而是一张清晰的主流程状态图用户每一步操作对应的页面状态是什么、前后状态之间的数据依赖是什么、预期结果在哪里出现。我先用一段伪代码把 profile1 的场景骨架列出来再讲具体的实现细节profile: profile1 flow: - step: 打开登录页 - step: 输入租户账号密码 - 点击登录 - assertion: 登录成功跳转至工作台首页顶部显示租户名称 - step: 在搜索框输入商品关键词 - 回车 - assertion: 商品列表页展示搜索结果且结果条数大于 0 - step: 点击第一个商品 - 进入详情页 - step: 点击加入采购单 - 弹窗确认数量 - 确认 - assertion: 购物车/下单页出现该商品数量正确 - step: 点击立即下单 - 选择收货地址 - 提交订单 - assertion: 订单提交成功页面展示订单号 - step: 点击去支付 - 模拟收银台拉起 - 支付成功 - assertion: 返回订单详情页状态显示已支付这个骨架确定了之后每个 step 对应的测试代码就只需要做两件事操作和断言。你要始终记得e2e 用例的价值主体是断言操作只是手段。如果一条用例断言太少它就只能证明流程没崩不能证明流程做对了。所以 profile1 里每一段操作后面我都会至少跟一个明确的断言而且尽量断言业务状态不只是断言元素存在。比如断言登录成功要用顶部显示了租户的名称而不是登录按钮消失了这两个的验证力度是完全不同的。3.2 核心用例代码详解与关键配置骨架定好了下面就是真实的代码写法和配置说明。我用 Playwright 的 JavaScript/TypeScript 版本做示例因为这是目前我要重点推荐的技术栈。第一段先看一个最简单的登录场景重点在于解释 Playwright 自动等待的能力和推荐写法import { test, expect } from playwright/test; test(profile1 - 租户登录并进入工作台, async ({ page }) { // 打开登录页 await page.goto(https://e2e.xxx.com/login); // 输入账号密码 await page.getByPlaceholder(请输入租户账号).fill(tenant01); await page.getByPlaceholder(请输入密码).fill(e2e_tenant_2024); // 点击登录按钮 await page.getByRole(button, { name: 登 录 }).click(); // 断言跳转到工作台且顶部显示租户名称 await expect(page).toHaveURL(/\/home/); await expect(page.getByTestId(tenant-name)).toHaveText(测试租户一); });这段代码看起来很简单但里面蕴含了大量 Playwright 为你兜底的细节。比如getByRole和getByPlaceholder这类基于可访问性语义的定位器是推荐优先使用的因为它不依赖 CSS class 或者 DOM 结构只要页面上的可访问性标记不变即使前端重构了样式用例也不会轻易挂掉。再比如expect(...).toHaveURL()这种断言Playwright 会自动轮询等待直到 URL 匹配期望值或者超时而不需要手动写循环等待逻辑。接下来看一个稍微复杂的场景跨页面状态的验证。在 profile1 里用户把商品加入采购单之后采购单的角标数量应该联动变化而且刷新页面之后角标和详情页的数据应该保持一致。这个场景就特别适合用 Playwright 的多页面和网络拦截能力来实现import { test, expect } from playwright/test; test(profile1 - 商品加入采购单后状态持久化, async ({ page }) { // 先拦截加入采购单的接口确保请求发出并成功响应 const responsePromise page.waitForResponse( (res) res.url().includes(/api/shopping-cart/items) res.request().method() POST ); await page.goto(https://e2e.xxx.com/products/10001); await page.getByRole(button, { name: 加入采购单 }).click(); await page.getByRole(button, { name: 确认 }).click(); // 等待接口成功然后断言接口返回的数据 const response await responsePromise; const data await response.json(); expect(data.success).toBe(true); // 断言页面上的角标数量变成了 1 await expect(page.getByTestId(cart-badge)).toHaveText(1); // 刷新页面断言角标和数据仍然保持一致 await page.reload(); await expect(page.getByTestId(cart-badge)).toHaveText(1); // 进入采购单详情页验证商品是否真实存在 await page.getByTestId(cart-badge).click(); await expect(page.getByText(自动化测试商品A)).toBeVisible(); });这里有两个值得注意的设计点。第一我在点击加入采购单之前提前声明了一个responsePromise这样能保证在事件发生的第一时间捕捉到接口调用而不是等点击完成之后再去找响应对象这就避免了竞态条件带来的 flaky。第二刷新页面之后的断言非常关键它验证的不只是当前页面的交互状态还包括 localStorage、服务端存储、初始渲染这几层之间的数据一致性。以前我在不写 e2e 的时候这类 bug 经常只在真实用户刷新时才会暴露现在用一条用例就能守住。说到profile1这个配置还有一个经常被忽略的细节执行超时时间和重试策略。e2e 用例跑在真实浏览器环境里脚本执行速度受网络影响很大所以我给 profile1 场景设计了一组专门的配置import { defineConfig } from playwright/test; export default defineConfig({ testDir: ./e2e, timeout: 60_000, // 单条用例执行超时 60 秒 expect: { timeout: 15_000, // 单个断言等待超时 15 秒 }, retries: 2, // 重试 2 次应对偶发性环境抖动 workers: 4, // 4 个 worker 并行执行不同用例 reporter: [ [list], [html, { open: never }], ], use: { baseURL: process.env.E2E_BASE_URL || https://e2e.xxx.com, trace: retain-on-failure, screenshot: only-on-failure, video: retain-on-failure, }, });用生活化一点的方式来理解这些参数retries: 2就像是给用例穿了件救生衣第一次没跑过系统会再给两次机会只有连续三次都挂了才判定为失败。trace: retain-on-failure则像是飞机上的黑匣子用例失败时会把整个执行过程完整录制下来排查问题时可以直接在 Trace Viewer 里看到当时的页面快照、网络请求和 console 报错。workers: 4意味着测试框架会同时开 4 个浏览器去并行跑不同的用例这里一定要考虑被测环境的承载能力不要盲目调大。3.3 CI 集成与报告输出用例在本地能跑通只是第一步。真正让 e2e 产生长期价值的是把它接入 CI 流水线让它成为每次发版前置检查的一部分。我的接入策略很有讲究不是所有合并请求都跑全套 profile1因为全量 e2e 再快也需要几分钟到几十分钟如果每个 PR 都全量跑开发同学的等待成本会成倍增加反而会让大家开始排斥这个测试体系。我采用的方案是分级触发。平时每个 PR 只触发冒烟级 e2e即 profile1 里最核心的 10 条左右用例覆盖登录、首页加载、单商品下单、撤回订单这些最高频路径这套用例要求 5 分钟内跑完只有往主干分支合并、准备打版本标签、或者发版后冒烟巡检的时候才触发完整 profile1 和 profile2保证重大变更前的深度回归。这样做既守住了质量门槛又没把反馈周期拖得太久。CI 里运行 e2e 的流水线我一直坚持用下面的步骤顺序拉取测试代码和被测应用最新构建产物部署或者复用当前环境的测试版本运行npx playwright install确保浏览器环境就绪执行npx playwright test --grep profile1上传 trace、视频、截图以及 HTML 报告作为流水线产物设置阈值成功率低于 95% 则流水线失败通知相关团队。一个实操细节e2e 环境一定要在用例开始前做好健康检查。我见过太多次因为被测服务还没启动完e2e 用例在第一步页面加载时就全部超时白白浪费了半小时。所以我的流水线里会有一步e2e-healthcheck直接用 curl 去请求被测环境的/healthz和首页路径确认 HTTP 200 之后再开始正式测试。报告输出方面Playwright 自带的 HTML Reporter 已经很好用但我还会额外把测试摘要推到团队的即时通讯群里方便第一时间感知结果。摘要里包含三块信息总用例数、通过率、挂掉的用例名和失败原因摘要支线再附上报告链接。这样连对自动化测试细节不太熟悉的同事也能通过一条消息快速判断这次发版是否安全。4. 常见问题与排查技巧——线上回归跑挂的十几个真实案例这一节我认真梳理了 e2e 测试从落地到稳定运行过程中我遇到过的最典型的问题。每一个问题背后都对应一个真实的踩坑故事我尽量把排查思路也讲清楚让大家以后遇到相似情况时有迹可循。这里先说一句最核心的经验e2e 测试跑挂80% 的情况不是被测系统的功能有问题而是测试脚本自身写得不够稳。4.1 等待策略的坑为什么我的用例总是偶发失败刚上手 e2e 的人最容易踩的坑就是等不到位。页面元素已经渲染了一半测试脚本就急着去点结果点了个寂寞。老一辈做法是到处加sleep(3000)治标不治本而且还会让整个测试套件越跑越慢。现代 e2e 工具已经内置了自动等待机制但自动等待也不是万能的它只能感知到 DOM 状态的变化感知不到业务状态的真实就绪。举一个我实际遇到的例子下单页面有个提交订单按钮点击之后页面会先发起一个异步请求然后弹出一个支付二维码。我第一次写断言时直接判断二维码是否可见结果经常失败。后来发现二维码组件在请求发出后就会被插入 DOM但二维码的图片内容要等接口返回并渲染完成后才会真实出现。自动等待只帮我等到元素出现但我要断言的是支付状态已经生效。这个问题的解法是用业务状态断言替代 UI 状态断言也就是等待特定接口的响应成功或者等待订单状态文本从待支付变为已支付而不是只盯着一个元素在不在 DOM 里。第二个常见的等待问题是页面加载完了吗。很多人习惯用await page.goto(url)之后就认为页面已经加载完成实际上goto返回的时候是页面 load 事件触发的时间点而前端框架大量使用异步数据渲染load 事件触发时页面可能还只有骨架屏。我的习惯是如果一个页面有关键数据需要渲染后再做断言就先用一个expect去等待核心文本或者核心元素可见这相当于在代码层面显式告诉测试框架这里必须先等到业务数据就绪才能继续往下走。4.2 测试数据污染同一条用例为什么换个时间跑就挂了数据污染是 e2e 稳定性最大杀手没有之一。我之前维护过一套订单查询的 e2e 用例用例逻辑很简单创建一个订单然后在列表页搜索这个订单的订单号断言它出现在结果第一行。这用例本地跑十次能过十次但是放到 CI 上隔几天跑一次就开始时不时失败了。排查了很久才发现问题出在数据隔离上。那条用例为了省事直接复用了别的用例创建的订单数据而另一条用例会在特定条件下把订单状态改成已取消一旦两个用例的执行顺序发生改变查询用例的断言就挂在订单状态不对上。这个问题的根治方案就是我前面提到的每条 e2e 用例都应该准备自己独立的数据。我后来把用例改成了通过 API 在 beforeEach 阶段创建一条带时间戳的订单测试结束再统一清理从那以后再也没遇到过这个场景的数据污染问题。数据清理这件事执行起来要格外细心。不要清理得太快也不要凭空清理。我的常见做法是在测试完成后调用删除接口清理但前提是删除接口本身不是被测试的对象否则就要换一种方案比如直接把测试数据写入一个单独的逻辑库定期统一回收这种模式在 B2B 平台里尤其适用因为它有明确的租户维度清理成本极低。另外请注意如果测试脚本依赖执行顺序或者说某条用例运行结束后的环境状态会影响下一条用例那这一定是一个设计错误用例之间必须做到完全独立。为保证这一点我会在每一条用例的断言里额外验证前置状态符合预期比如下单用例会先断言登录状态、库存数量、地址列表都符合预期再开始主流程。4.3 并行执行与资源隔离worker 开多了为什么反而更慢引入 Playwright 之后我一开始非常贪心地开了 8 个 worker 并行跑全量用例想着能最大化利用机器资源。结果非常反直觉——总执行时间并没有显著下降反而局部出现了更多 timeout 和断言失败。排查下来发现是测试目标应用扛不住了8 个浏览器同时发起大量请求把测环境的接口服务直接打到了高负载状态部分请求响应时间超过了断言等待上限用例就挂了。这个问题教会我一件事并行度不是越高越好它应该根据被测系统的承载能力来设定。我的调整办法是先观察单个 worker 跑全量用例时的基础耗时然后逐步增加 worker 数找到吞吐量开始下降的那个临界点再回退一档作为默认配置。目前那个 B2B 平台的线上配置就是 4 个 worker实测是最优平衡点。另外还有一个不被很多人注意的细节并行跑的时候不同 worker 的浏览器进程之间其实是互相隔离的但如果你的测试代码里出现了共享的全局变量、磁盘临时文件、或者同一个数据库里的固定名称纪录污染风险会成倍增加。所以并行方案落地前一定要先做一次串行全量通过、并行重复跑三遍的稳定性验证。还有一个经常被问到的点验证码、短信验证码这类需要人工介入的环节怎么处理。我的方案很直接测试环境下拉一个验证码开关所有 e2e 环境统一走万能验证码000000这个开关只在测试环境生效并且通过 mock 短信服务实现绝不触碰真实短信网关。如果你的项目没法做这种开关也可以走预留测试手机号 邮箱解析验证码的方案但那种方式维护成本更高不推荐首选。总之e2e 测试理论上是自动化闭环凡是无法自动化的外部依赖都应该在设计阶段就把它隔离掉或者替换掉。4.4 用例维护测试套件越来越大如何保持健康最后聊一个所有 e2e 体系都会面临的长期问题用例数量上去了维护成本怎么控制住。我见过不少团队一开始热情高涨写了三四百条 e2e 用例但不到半年就变成了趟雷区动不动全挂维护者每天都在改 selector、加等待、调整断言累死累活最后还是被领导点名自动化测试形同虚设。这个问题不是工具不行而是用例设计和治理从一开始就没做好。我的经验有三条。第一坚持分层设计不要把所有验证都堆在 e2e 层。e2e 的主角是跨层链路验证业务逻辑的细节尽量下移到接口测试或者组件测试去覆盖。比如一个按钮的文案、一个表单的校验规则、一个金额计算的边界值这些在接口层和单测层就能覆盖得很好没必要在浏览器里反复测。保持 e2e 用例的小而精执行时间才能闭环在可控范围。第二建立用例的 owner 制度。每条用例都挂到具体业务模块的负责人名下哪个模块的用例挂了对应负责人要去修而不是指望一个专职测试把所有模块的业务逻辑都吃透。我所在团队的实践是每个业务小组可以指定一名 e2e 维护接口人每周轮换这样责任感是分散的不会变成某个人的独角戏。第三定期做用例清理。每隔一个迭代我会把所有用例的执行历史拉出来按照最近 30 天运行次数、失败率、最后修改时间三个维度排序把确认没有业务价值的用例直接删除或者降级为手动场景。这个习惯虽然有点反直觉因为大家总觉得用例越多越踏实但实际情况是一条验证价值很低、执行频率却很高的用例只会不断消耗大家的信任和耐心不如把它拿掉。说到清理我想起一个具体情况平台改过一次商品详情页的信息架构把加入采购单按钮从页面右上角移到了商品信息区的左下角同时还改了按钮的文案加了图标。当时我们团队正好有十几条用例都是通过page.click(text加入采购单)这种方式定位的结果一夜之间全部挂了。这一轮维护虽然糟心但让我养成了一个好习惯所有关键定位优先使用>