1. 先校准预期:e2e测试不是“多写几条用例”这么简单
只要你在大厂待过,或者参与过稍微成熟一点的Web项目,肯定会碰到这样一幕:CI流水线里一长串用例红成一片,开发扫一眼说了句“这是e2e挂了吧,我本地没问题”,然后顺手把用例标注成flaky。久而久之,整个团队对e2e测试的态度会从“必须做”变成“做了也是摆设”。我见过不止一个项目,e2e用例数量从几百条慢慢萎缩到几十条,最后只剩一条冒烟用例撑门面。
这不是e2e测试本身有问题,而是很多人从第一步就做错了——把e2e当成“用浏览器把功能点一遍再写个断言”,却不知道端到端测试的第一原则是克制。
1.1 e2e测试到底在验证什么
端到端测试,英文全称End-to-End Testing,中文俗称“全链路测试”。它的核心价值不是“覆盖率高”,而是“链路通”。什么意思呢?一个用户从打开页面开始,经过登录、输入、提交、等待后端返回、页面刷新、看到结果——这一整条跨模块、跨服务、跨网络的真实链路,前端自测覆盖不了,单元测试覆盖不了,接口测试也只覆盖了其中一段。e2e就是要站在用户操作的视角,把这条链路整体跑一遍,验证各个模块拼在一起之后没有断点。
一个很经典的案例:前端和后端各自的测试都绿,但只要前端把某个字段名从userId改成uid,后端没同步改,接口层就静默失败了。前端单元测试用的是mock数据,根本发现不了;后端接口测试直接传uid,也没问题;只有e2e测试从登录开始走真实交互,拿到真实响应,才会在这个字段上炸出来。这就是e2e不可替代的价值——它不是“测试的补充”,而是“集成后的真相”。
1.2 单元测试、集成测试与e2e的边界怎么划
很多人写e2e之前没有想清楚分层,导致用例写得像“套了一层壳的单元测试”。我建议用一张简单的对照表来定边界:
| 测试类型 | 验证目标 | 典型粒度 | 稳定程度 | 运行速度 |
|---|---|---|---|---|
| 单元测试 | 函数/类的输入输出逻辑 | 单个函数 | 高 | 秒级 |
| 集成测试 | 模块间交互、数据流转 | 多个模块/服务 | 中 | 分钟级 |
| 组件测试 | 单组件渲染与交互 | 单个组件 | 中高 | 秒级 |
| e2e测试 | 用户真实操作链路 | 完整业务流程 | 低(波动最大) | 分钟级 |
这里的“稳定程度”不是贬义,而是客观事实。单元测试只依赖一个函数,你给它什么输入它就返回什么输出,几乎不受外部环境影响;e2e测试要驱动浏览器、要等网络、要等后端、要处理异步渲染,任何一个环节抖动都会导致用例失败。所以e2e的定位应该是“守住关键业务主链路”,而不是“把每个按钮都点一遍”。
我见过最典型的错误是:团队把e2e当成万能测试工具,什么功能都往里面塞,最后维护成本爆炸。正确做法是:能用单元测试覆盖的逻辑,绝不上e2e;只有单元测试和集成测试都覆盖不到的“跨端链路”,才轮到e2e出场。说白了,e2e测试的每一行代码都贵,应该花在刀刃上。
1.3 为什么有人觉得e2e“没用又难维护”
“没用”和“难维护”其实是同一个问题的两个面。当你把e2e用例写得过细、过碎,比如登录后检查首页每个按钮的颜色,那这个用例对业务价值的贡献几乎为零,但每次前端样式微调它都会红一次。你每天花半小时修这类用例,自然觉得它“没用”。
还有一个常见的认知偏差:很多人以为e2e失败就等于“测试脚本写错了”,于是急着去改脚本。实际上,e2e失败有相当比例暴露的是真实缺陷或者环境问题。我统计过自己负责的项目,e2e用例失败原因大致是:真实缺陷占30%、测试数据/环境问题占40%、脚本不稳定占30%。如果你只在跑完的时候瞥一眼,不去深挖失败根因,那确实会把e2e的价值白白丢掉。
所以,做e2e测试之前,先调整预期:这是一套需要持续投入、持续治理的测试体系,不是一个“写完了就躺着跑”的自动化工具。定义清楚它该守什么、不该守什么,后面所有技术选型和方案设计才有依据。
2. 动手前的关键决策:哪些场景值得写e2e,哪些场景不该写
聊完了定位,接下来是e2e实施中最容易踩翻车的环节——选场景。很多人拿到任务就开始写用例,先登录、再进列表、再点详情、再提交表单,一口气把整个系统的核心功能都覆盖了一遍。听着很全面,实际上是把测试资源撒胡椒面。e2e用例不是越多越好,而是越“准”越好。
2.1 用商业风险而不是代码覆盖率来选场景
选e2e场景的标准,不是“这段代码改动频率高”,也不是“这个方法分支多”,而是:如果这条链路坏了,用户损失有多大,公司损失有多大。举几个典型的“高商业风险”场景:
- 注册/登录链路:用户进不来,一切白搭;
- 支付/下单链路:钱收不到,订单丢失,直接经济损失;
- 主流程操作:比如电商的加购物车、结算、物流查询,任何一个环节卡住都影响转化;
- 权限控制:普通用户能访问管理员页面,这是安全问题,比功能出错更严重。
反过来,像“个人中心头像修改”“用户备注编辑”这类低频、低影响的功能,即使e2e覆盖了,价值也很有限。我见过一个团队把“用户昵称修改”做成了一整套e2e用例矩阵,从正常修改、修改为空、修改为超长字符串、修改为特殊字符,写了十几条用例——这部分逻辑明明用单元测试三分钟就能覆盖,e2e跑一遍还要额外花几十秒。这就是典型的场景选择失误。
实际操作中,我建议用“用户旅程地图”来辅助选样:把用户从首次进入产品到完成核心目标的路径画出来,凡是这条主路径上的关键节点,值得写e2e;旁边的支线、低频分支,能用单元测试覆盖就用单元测试。你甚至可以先问自己一个问题:明天上线前只允许跑10条e2e用例,我会挑哪10条?这个选择题做完了,场景清单也就出来了。
2.2 一个典型的e2e用例目录长什么样
选定场景之后,用例目录结构也很重要。好的目录不是按功能模块堆的,而是按“业务流程”组织的。我习惯这样组织:
e2e/ ├── config/ # 配置文件(环境地址、账号凭据、超时时间等) ├── fixtures/ # 测试数据(用户态、接口Mock、SQL重置脚本) ├── utils/ # 通用操作封装(登录、上传、日期选择等) ├── specs/ │ ├── auth/ │ │ ├── login.spec.ts │ │ ├── reset-password.spec.ts │ │ └── logout.spec.ts │ ├── checkout/ │ │ ├── add-to-cart.spec.ts │ │ ├── create-order.spec.ts │ │ └── pay.spec.ts │ └── admin/ │ └── permission-check.spec.ts └── reports/ # 测试报告输出目录注意这里有个细节:auth目录下的测试不叫“登录功能测试”,而是“登录链路测试”。前者关心输入框和按钮本身,后者关心“从打开页面到进入首页”这条链路的完整性。命名方式会直接影响你组织用例的思路。
另一个细节是:每个spec文件里不一定只有一条用例,但每个用例的断言应该尽量少。我见过有人写一条用例,一口气断言了20个UI元素存在。这种用例一旦失败,定位问题要花很长时间。我建议一条e2e用例只验证一个核心结果,最多加一到两个辅助断言。比如支付用例,核心断言是“订单状态变为已支付”,辅助断言是“页面出现支付成功提示”——仅此而已。
2.3 数据准备和环境管理:最容易被低估的环节
如果说场景选择决定了e2e的价值上限,那数据准备和环境管理就决定了e2e的稳定性下限。我见过无数e2e项目死在“测试数据被污染”上:昨天跑得好好的用例,今天因为数据库里多了一条脏数据,列表页多了一行,元素位置变了,用例挂了。这种问题跟你的脚本无关,但你不解决,它就会一直缠着你。
先说数据准备,核心原则是:e2e用例的测试数据必须可重复、可隔离、可清理。我推荐三种方式结合使用:
- 针对只读场景,使用固定的种子数据(比如测试账号、测试商品、测试订单),这些数据由测试环境部署脚本统一初始化;
- 针对写操作场景,每个用例运行前通过接口或数据库脚本创建独立的测试数据,用例结束后清理;
- 对于特别复杂的业务数据,直接用Mock接口返回可控数据,前提是你清楚Mock的边界——Mock掉的正好是你想验证的链路,那就失去e2e的意义了。
再说环境管理,最简单也最推荐的做法:给e2e测试准备一个独立的环境,不跟开发环境混用,不跟手动测试环境混用。这个环境的数据可以被随时重置。配合一个叫TEST_ENV=profile1之类的配置切换机制,让同一套测试代码可以指向不同环境(本地、测试、预发布),核心是配置文件里把环境地址、账号凭据、超时阈值都集中管理。
我见过有些团队把e2e跑在开发环境上,开发同事正在调接口,直接把某个表锁了,e2e用例立刻红一大片。这种环境耦合造成的失败,是e2e治理成本里最大的一块。所以我的建议很直接:宁可多花点时间搭环境,也不要在脏环境上硬跑。
3. 工具链选型与基础搭建:我为什么最终选了Playwright
工具选型这件事,说简单也简单,说复杂也复杂。简单是因为主流的工具就那几个,各自的能力边界很清楚;复杂是因为不同团队用的技术栈、部署方式、团队习惯差异很大,照搬别人的选型容易水土不服。
3.1 主流框架对比:Playwright、Cypress、Selenium的关键差异
先看一张对比表,这是我实际使用这些工具之后的感受:
| 维度 | Playwright | Cypress | Selenium |
|---|---|---|---|
| 支持语言 | JS/TS、Python、Java、C# | JavaScript/TypeScript | 多种语言 |
| 自动等待 | 内置Web-First断言和等待 | 内置重试与超时 | 需要手动显式等待 |
| 多标签/多浏览器 | 支持,可操作Chromium、Firefox、WebKit | 不支持多标签 | 支持,但配置繁琐 |
| 浏览器隔离 | 支持BrowserContext,天然隔离 | 不支持 | 基本不支持 |
| 网络拦截 | 支持Route/Context拦截 | 支持cy.intercept | 需要代理方案 |
| 并行执行 | 内置workers并行 | 支持,需要扩展 | 需要结合TestNG等 |
| 调试体验 | Playwright Inspector + Trace Viewer | 内置时间轴 | 相对较弱 |
我个人的结论是:新项目优先选Playwright,没有特殊理由。原因是它解决了e2e测试里最头疼的几个问题:自动等待、上下文隔离、网络拦截、并行执行。这几个能力直接影响测试的稳定性,而稳定性恰恰是e2e落地最大的痛点。
Cypress在交互体验上做得很好,实时重载、时间旅行调试都让人上瘾,但它的架构决定了只能在Node环境跑,不能跨多标签页,移动端支持也有限。如果项目是纯前端、纯浏览器场景,Cypress完全可以;但如果要跑多浏览器兼容或移动Web场景,Playwright更有优势。
Selenium依然是很多老项目的标配,稳定性经过了多年验证,但“手动等待”这一点真的会让人抓狂。Selenium的显式等待写多了,代码里全是WebDriverWait,可读性差、维护成本高、还容易因为时间估算不准而误判。除非项目已经是Selenium的既有体系、迁移成本太高,否则我不建议新项目从零选Selenium。
3.2 测试账号与用户态管理
选型之后,第一个要搭的基建就是“测试账号和用户态管理”。你可能觉得,登录嘛,用一个测试账号调一下login接口拿到token再塞进浏览器LocalStorage不就好了。理论上是这样,但实操里有个大坑:很多系统在登录后还有额外的状态初始化(比如用户设置、推荐流、环境标记),单纯塞token不够,页面初始化会报错。
更靠谱的做法是把你自己的登录流程封装成一个可复用的“请求级登录”工具:
// utils/auth.ts import { request } from '@playwright/test'; export async function loginAs(profileKey: string) { const profile = getProfile(profileKey); // 从配置文件读取账号信息 const response = await request.post(profile.baseUrl + '/api/login', { data: { username: profile.username, password: profile.password, }, }); const token = (await response.json()).token; // 返回一个回调,由调用方注入到对应页面上下文 return async (page) => { await page.addInitScript((t) => { localStorage.setItem('token', t); localStorage.setItem('userProfile', JSON.stringify(profile.user)); }, token); }; }这样做的好处是:一个spec文件里可以灵活切换不同用户态(普通用户、管理员、VIP用户),而且不用每次都重跑UI登录流程,节约大量执行时间。但注意,addInitScript只是预置本地状态,如果应用启动时会向后端拉取用户信息,你还需要用page.route把对应的用户信息接口Mock掉,或者在真正的请求里带上有效token。我建议优先走“有效token+不Mock业务接口”的方式,这样链路是通的;只有接口不可控时才考虑Mock。
这里特别要提一下,测试账号的凭据不能硬编码在spec里,更不能提交到git仓库。我会用一个profiles.json文件统一管,并且把真实密码放在环境的机密变量里,测试运行时动态注入。这样既方便团队同事读取,又不会把敏感信息泄露出去。
3.3 CI流水线里跑e2e的基本姿势
e2e测试如果没有接入CI,那基本等于没写——因为你在本地跑几百遍都是绿的,不代表提交代码那天它还是绿的。但直接把e2e挂到每个commit上,又会把构建速度拖垮。我的经验是分三层:
- 冒烟层(每次push触发):只跑核心冒烟e2e,控制在几分钟内,覆盖面是登录、主流程、最关键的1-2条链路;
- 全量层(PR合并前/每日夜间触发):跑完整的e2e套件,可以并行分片(shard)来压缩时间;
- 手动触发层(发布前):在预发布环境跑全部用例,重点验证待发布版本与线上配置的兼容性。
这个分层和单元测试的分层逻辑是一样的。如果你的团队对“每次push都跑冒烟”有意见,先别急着砍,优化用例数和执行时间是更合理的解决方案。
CI里还会遇到一个经典问题:e2e测试需要访问被测环境的地址,且这个地址可能不是公网的。比如测试环境部署在K8s集群内部。我的做法是在CI里额外启动一个转发代理(或者直接让e2e容器运行在与被测环境相同的网络命名空间里),保证访问可达。这类环境基建问题,必须在开始写用例之前就解决掉,不然写再多用例都白搭。
4. 脚本结构设计:让e2e用例能活过三个迭代
有了工具和环境,下一步就是设计脚本结构。这一步决定了你的e2e测试是能长期维护,还是三个月后推倒重写。
4.1 Page Object模型在不同团队规模下的取舍
Page Object Model(页面对象模型)是e2e测试里非常经典的架构模式:把页面元素定位和交互动作封装成一个类,spec只关心业务操作,不关心DOM细节。比如:
// pages/login-page.ts import { Page } from '@playwright/test'; export class LoginPage { private readonly page: Page; constructor(page: Page) { this.page = page; } async goto() { await this.page.goto('/login'); } async fillUsername(username: string) { await this.page.getByLabel('用户名').fill(username); } async fillPassword(password: string) { await this.page.getByLabel('密码').fill(password); } async submit() { await this.page.getByRole('button', { name: '登录' }).click(); } async login(username: string, password: string) { await this.goto(); await this.fillUsername(username); await this.fillPassword(password); await this.submit(); } }Page Object的好处是:当页面元素变动时,你只需要改一个文件,所有引用它的spec自动修复。这个收益在两周以上的项目中就会非常明显。
但我也要提醒:Page Object不是银弹。如果你的团队只有两三个人,且被测系统是轻量的内部管理后台,那直接在每个spec里写元素定位反而更快。过度封装会导致抽象层级太多,改一个元素要改三层类,效率反而低。我的建议是:等spec里重复出现三次以上同样的元素操作时,再提取Page Object。优先保持代码简单,而不是优先追求架构完美。
4.2 断言策略与“可重试”机制
e2e测试的断言策略跟单元测试很不一样。单元测试可以断言某个函数“不抛异常”就通过,e2e测试更关心“用户实际上看到的结果”。
我常用的断言分层是这样的:
- 核心结果断言:比如支付成功后“订单状态变为已支付”——这类断言用框架提供的自动等待断言(如
expect(locator).toHaveText()、expect(page).toHaveURL()),它们会自动重试直到超时,所以稳定性最高; - 辅助状态断言:比如页面顶部出现成功提示条——这类信息对业务价值不高,建议用软断言或者干脆不做断言;
- 防御性断言:比如操作之后页面没有出现报错弹窗——这类断言最有价值,因为它能捕捉到“功能虽然完成但前端报了错”的场景。
关于重试,我特别想说一句:不要滥用“整个用例失败后自动重试”这个机制。它治标不治本,而且会掩盖真实缺陷。正确做法:针对已知的、低频的、无法通过等待解决的偶发问题,在单个用例级别配置最多两次重试;对于频繁失败(比如超过20%比例)的用例,不重试,直接进失败分析,因为这意味着用例本身有稳定性问题,需要修脚本或修测试数据。
4.3 用例间依赖管理
我见过最糟糕的e2e代码长这样:第一个用例创建订单,第二个用例直接复用第一个用例的订单数据,第三个用例又依赖第二个用例的状态。这就是典型的用例间依赖。它带来的问题是:第一个用例失败,后面所有用例跟着失败,排查时根因会互相干扰。
我的原则非常明确:每一条e2e用例应该可以独立运行。用例之间可以共享数据,但共享的方式应该是“公用fixture”,而不是“前一条用例的输出”。比如订单相关的用例都需要一个已创建订单,那就写一个createOrderFixture,各用例各自调用,而不是让第一条用例负责“生产”订单给后面的用例用。
// fixtures/order.ts import { test as base } from '@playwright/test'; export const test = base.extend<{ testOrder: Order }>({ testOrder: async ({ page, request }, use) => { const order = await createOrder(request); // 通过API创建,不依赖UI await use(order); await cleanupOrder(request, order.id); // 用例结束清理 }, });把这个逻辑封装成fixture之后,每个用到订单的用例都“自己准备数据、自己清理”,互不干扰,也能独立运行。这才是可维护的e2e结构。
5. 真实踩坑记录:我把失败的e2e用例分成了这几类
工具和能力都到位之后,进入最考验耐心的一环:处理不稳定用例。这里我把自己踩过的坑和排查链路完整写出来,按失败类型整理,希望你能少走弯路。
5.1 元素定位不稳定:从选择器优化开始
有一次我们跑后台列表页的e2e,用例内容是从第一页翻到第二页,然后断言第二页第一条数据的文本。用例时好时坏,红的时候报错是“找不到元素”。我刚开始以为是网络慢,把超时时间从10秒加到30秒,没用。后来打开失败截图一看,发现列表页压根没翻页——原因是“下一页”按钮被一个浮层的阴影挡住了。
这类问题通常不是脚本写错,而是元素定位方式选错了。我们的代码里用的是page.locator('.pagination-next'),这个类名在很多页面都有,Playwright匹配到的是隐藏的那个。改成page.getByRole('button', { name: '下一页' })之后,问题立刻消失。
排查得到的一个教训:优先使用用户可见的语义定位(getByRole、getByLabel、getByText),其次才是CSS选择器,尽量不要用绝对定位的XPath。因为可见文本和可访问性标签更接近用户的真实操作方式,改动时也更容易跟上业务变化。
5.2 前端异步渲染与等待策略:别再用sleep
前端框架(Vue、React)的异步渲染是e2e测试最大的坑之一。点击按钮之后,数据请求发出去,响应回来,组件更新DOM——这个过程不是同步的。很多人图省事,直接page.waitForTimeout(3000)。这个方案的问题是:一旦响应超过3秒就挂,响应快时又白白多等3秒,执行时间被拖长。
正确的做法是等待用户可感知的状态。页面要出现“加载中”动画,那就等它出现再等它消失;页面要出现数据列表,那就在断言前等待列表中的某一条数据出现。Playwright的自动等待机制已经很强大了,你要做的是把“等待”绑定到业务状态上:
// 好的例子:等待列表刷新后的第一行文本出现 await expect(page.getByRole('row').first()).toHaveText('订单号:10086');这段代码会自动重试,直到第一行出现期望文本或者超时。你不需要手动sleep,也不需要在点击和断言之间插入任何“休息”。如果一定要用waitForTimeout,我建议只在调试阶段用,正式代码里不要出现。
5.3 接口数据污染与隔离
这是e2e项目后期最磨人的问题。早期用例少,数据混在一起也能跑;用例多了以后,A用例创建的订单,B用例在列表里也能看到,导致B用例断言“列表为空”永远失败。
排查链路是这样的:先看失败截图和录屏,发现页面确实多了一行数据;再去数据库查,发现这条数据是上一个用例创建的,没清理干净;最后翻日志,发现清理逻辑只在用例成功时才执行,用例一旦失败就跳过了清理步骤。
解决方案分两层:
- 第一层,在fixture的清理阶段用
try/finally确保即使用例失败也执行清理。上面的订单fixture里,cleanupOrder无论use内部是否抛错都要跑; - 第二层,在跑全量e2e之前,先执行一次环境数据重置。最理想的情况是每个测试用例都运行在完全干净的数据环境里(比如每次启动独立测试数据库),实在做不到就用“数据前缀隔离”——每个用例创建数据时都带唯一前缀,断言也按前缀过滤。
5.4 并行执行带来的隐性竞争
把e2e用例并行跑能大幅压缩时间,但也引入了新的坑:多个浏览器上下文同时操作同一个应用后端,如果后端有全局状态(比如全局唯一编号、库存扣减),用例之间就会互相踩脚。
我遇到过一个具体案例:两条支付用例并行执行,都创建了同一个外部支付单号,后端服务去查重,直接拒绝了一个订单,用例失败。单独跑两条用例都是绿的,一开并行就红。
这类问题的排查思路是:
- 先确认并行使用的共享资源是什么——数据库记录、Redis缓存、外部服务调用;
- 再确认是“数据竞争”还是“资源耗尽”;
- 针对数据竞争,优先让数据生成具备唯一性(用时间戳/UUID作为单号前缀);
- 针对资源耗尽(比如后端同一时间只允许一个请求处理),那就调整并行策略,把这类用例串行执行,或者给它们打上独立的测试标识隔离资源。
在Playwright里,我一般把用例划分到不同的“项目/分组”,将高风险共享资源的用例放到同一个worker且串行执行,其余用例再开并行。粒度控制比一锅乱跑要安全得多。
6. 成本控制与持续演进:e2e测试的长期维护策略
最后聊聊e2e的运营之道。写用例不是终点,让用例持续稳定地发挥价值才是终点。这需要你把e2e当成一个长期运行的产品来治理,而不是一次性交付的测试代码。
6.1 运行时长优化:从“跑得动”到“跑得快”
全量e2e套件跑太久,会直接导致团队反馈变慢。优化手段我按收益从大到小排列:
- 并行分片:把用例均匀分配到多个worker上,一台8核机器大概能压掉60%的时间;
- 请求级登录:把UI登录替换成API登录,每一条用例能省下5~10秒;
- 跳过不必要的屏幕截图/视频:只在失败时保留录屏和截图,平时不录,能省不少IO时间;
- 清理无价值用例:定期梳理用例清单,凡是已经连续三个月没有失败过的用例,考虑降级为低频跑用例或直接删掉。这个操作听着反直觉,但确实是减少维护成本的有效方式——低频用例的脚本和定位器可能已经过时,平时不跑根本发现不了,保留它们只会积累技术债。
6.2 失败快速定位:报告和日志的工程化
写完用例之后,最花时间的其实是“失败定位”。如果每次失败都要打开远程虚拟机、手动登录去复现,那e2e的维护成本会高到让人崩溃。我的做法是让失败信息“自解释”:
- 失败时自动保存页面截图、DOM快照、控制台日志、网络请求记录;
- 使用Playwright的Trace Viewer,把完整的操作轨迹变成可回放的trace文件,定位时直接拖时间轴看每一步发生了什么;
- 在spec里写清楚前置条件和预期结果,保证任何一个人拿到失败报告都能理解业务场景,而不需要问写用例的人。
我见过很多团队在e2e报告里只放“元素找不到”这一行报错,排查的人只能一脸懵。通过给报告增加上下文信息,其实能解决一大半“不稳定用例”的争议——当一个失败能被完整回放并解释清楚时,是脚本问题还是环境问题,立刻有结论。
6.3 维护节奏与团队协作:把e2e纳入代码评审
e2e测试代码也是代码,应该走完整的代码评审流程。我特别主张:改动页面结构时必须同步评估e2e影响面。很多团队的前端评审只看功能逻辑,没人关心测试代码,等页面重构完,e2e红一片,才想起来“哦,测个试试”。
另外,我建议把e2e的“运行报告趋势”拉到一个团队可见的地方,每周花一个固定的短会时间看一眼。重点看两个指标:失败率和修复时长。失败率超过10%的用例,要么这周内修复,要么直接标记为待删除。修复时长超过一天的用例,说明用例本身设计有问题,需要重构。这类维护节奏是e2e体系能够长期健康运行的关键。
一个行之有效的团队规则是:“你打破的,你来修”。谁在改动业务代码时导致e2e用例失败,就由谁优先修复相关脚本或同步给测试团队说明原因。这能逼着开发在提PR之前主动检查e2e状态,而不是等CI跑挂了再互相推诿。
从我个人的体会来说,e2e测试从来不是一个“写脚本”的活儿,而是一个“经营稳定性”的活儿。前期花一周搭好环境、做好数据管理,中期坚持用高质量定位器和业务化断言,后期每周花固定时间治理失败用例,这套体系才能真正起到“守住业务主链路”的作用。如果你一开始就跑得很野、没做好数据隔离、没控制用例数量,那后面大概率会被维护成本拖垮。先把最核心的10条链路跑稳,再逐步扩展,是e2e落地最稳妥的路径。