1. 先从"到底要不要上E2E"说起:端到端测试的定位与成本
入行这些年,我见过太多团队对 e2e(端到端测试)的态度在两个极端之间摇摆:要么觉得它就是银弹,一口气把整个业务线全部自动化,结果一个月后维护成本爆炸;要么被它的脆弱性折磨到怀疑人生,最后所有用例全部注释掉,留下一堆没人敢碰的代码。说句实话,e2e 这门手艺,难的不是写用例,而是搞清楚它到底该在质量体系里站哪个位置。
很多人把 e2e 理解成"模拟用户点鼠标"的工具,这个理解不算错,但太浅了。端到端测试的本质,是站在真实用户的位置上,用真实的浏览器、真实的网络、真实的数据,完整走一遍用户会走的路径,验证整个系统从下到上、从数据库到前端渲染是否协调一致。它不关心某个函数返回值对不对,也不关心某个接口响应是否 200,它关心的是:一个用户打开网页之后,能不能顺利注册、登录、下单、支付,中途不会因为任何一环失灵而卡住。
1.1 E2E 在测试金字塔里的真实位置
经典的测试金字塔模型里,底层是大量的单元测试,中间是集成测试,塔尖才是 e2e。为什么是金字塔而不是个桶?因为越往下,测试成本越低、运行速度越快、稳定性越高;越往上,覆盖范围越广,但速度越来越慢、维护成本越来越高、失败时定位问题也越来越难。
所以 e2e 从来不应该追求"数量多",而应该追求"位置准"。我的习惯是:单元测试覆盖 70% 的纯逻辑,集成测试覆盖 20% 的模块交互,e2e 覆盖剩下的 10% 关键用户旅程。如果你反过来做,把几千条用例全压在 e2e 上,那就等着 CI 跑两小时、每天都有一堆随机失败等着你修吧。
1.2 什么时候不该上 E2E
这个反直觉的结论先说在前头:不是所有项目都适合 e2e。我见过最荒唐的案例,是有人给一个只有三个静态页面的官网写了二十多条 e2e 用例,每次改个文案都得跟着改断言,后来广告文案一天改三次,测试就一天红三次,最后全被删了。
以下几种场景,我建议你先别急着写 e2e:产品还在原型验证阶段,页面结构三天两头推倒重来;业务逻辑极其简单,没有跨模块的数据流转;团队只有一两个人,连 CI 都没有;又或者是那种临时性的活动页,活不过一个月。E2E 是重资产,它的产出是"长期回归信心",如果项目本身生命周期很短,或者页面还在剧烈变化,这笔买卖是亏的。
反过来,如果你的系统里存在一条真正的"核心用户路径"——比如电商的逛、加购、下单、支付;比如 SaaS 的注册、建工作区、邀请成员、发第一条消息——而且这条路一旦挂掉就等于业务中断,那这时候 e2e 不是选项,而是必需品。项目规模越大、人越多、发布越频繁,越需要它来兜底。
2. 选型不是选最好,是选最不折腾:Cypress 与 Playwright 的取舍笔记
工具选型这事,我踩过的坑比写用例踩的还多。从最早的 Selenium,到后来一度很火的 Cypress,再到现在的 Playwright,每个工具都有自己的脾气,关键是搞清楚你的团队在意什么、痛点是什么。没有"最好的框架",只有"跟你团队免疫力最匹配的框架"。
2.1 Selenium:能跑,但折磨人
Selenium WebDriver 是元老级方案,它把浏览器操纵暴露成标准协议,理论上什么语言、什么浏览器都能支持。很多大公司遗留的测试框架都是 Selenium。但它的问题也很典型:各种隐式等待、显式等待写得让人精神分裂;对弹窗、下载、文件上传这类场景的支持比较原始;报告和调试体验基本靠外部插件拼凑;分布式跑用例还要自己搭 Grid。除非公司已经有成熟的 Selenium 基础设施,否则我不太推荐新项目从它起步。
还有一个隐藏的坑:Selenium 只管"操作浏览器",断言、等待、重试、报告全都要自己集成。如果你的团队是测试团队主导、有专门的基础设施工程师,Selenium 是可控的;但如果你只是几个前端工程师想快速把回归跑起来,用它就是给自己找罪受。
2.2 Cypress:前端开发的利器,但有天花板
Cypress 当年火起来不是没道理的。它最大的特点是把测试跑在浏览器进程内,能感知应用内部的一切,所以调试体验极其顺滑——失败的时候你可以像看录像一样回放每一步操作,还能打开 devtools 直接查当时页面上的状态。对前端工程师来说,写 Cypress 用例的体验跟写业务代码几乎一样爽,这也是它当年横扫社区口碑的原因。
但 Cypress 的天花板也很明显。它默认只能在它自己的 Runner 里跑,多标签页支持一直很别扭;如果你想做移动端真机测试、或者要跑到 Safari 上,那就更麻烦了。跨域导航上的限制,让很多涉及第三方登录、嵌多个 iframe 的场景写得非常难受。我项目里有个单点登录流程,所有 iframe 里的断言在 Cypress 里都要靠变通手段,维护起来疼得厉害。
2.3 Playwright:综合成本最低的选项
后来我整体迁移到了 Playwright,现在已经成了我的默认答案。它由微软团队维护,最核心的几个设计非常对路:内置自动等待,不用再写那些让人崩溃的sleep(3000);浏览器支持极好,Chromium、Firefox、WebKit 一网打尽,也就是说你可以在 CI 里顺手把 Safari 内核也覆盖了;网络拦截能力非常强,mock 接口、模拟弱网、伪造请求失败都特别方便。
Playwright 还有一个杀手锏:它把"一键录制用例"做得很实用。录出来的代码虽然不能直接用,但可以快速帮你生成定位器和操作骨架,省掉大量的查文档时间。再加上 trace viewer 能看到每一步前后的页面快照和网络请求,排查问题的时候舒服得多。下面这张表是我当年做选型对比时整理的,可以直接抄:
| 维度 | Selenium | Cypress | Playwright |
|---|---|---|---|
| 浏览器覆盖 | 全,但需自己管理驱动 | 主流,跨域场景受限 | Chromium/Firefox/WebKit 全支持 |
| 等待策略 | 手动为主 | 自动重试 + 手动混合 | 内置自动等待,心智负担最低 |
| 调试体验 | 一般 | 时间旅行回放非常强 | trace 回放 + 分步截图,很强 |
| 网络 mock | 依赖第三方库 | 支持,但有跨域限制 | 原生支持,拦截/伪造/模拟弱网都很顺 |
| 多标签/弹窗 | 支持较原始 | 支持弱 | 原生支持,API 很顺手 |
| 录制生成代码 | 不如后两者智能 | 有,但骨架粗糙 | 智能录制,可直接改造成 POM 结构 |
| 维护成本 | 高 | 中 | 中低 |
| 我个人的推荐度 | 存量项目才考虑 | 纯前端小团队可用 | 默认选项 |
2.4 选型的底层逻辑:谁在维护、多久改一次、跑在哪
选型从来不看官网 PPT,要看三个现实问题:第一,这个测试代码多数时候是前端工程师写还是专职 QA 写?如果是前端写,开发体验和调试体验的权重就要提到最高;第二,项目里有没有那些"很特殊的场景"——多个 iframe、跨域、下载文件、桌面通知?这些场景一旦存在,塞进 Cypress 里可能让你生不如死;第三,CI 环境是什么?如果你们的 CI 是 Docker 化的,那 Playwright 的官方镜像就能让你少掉一多半头发。
我当时的迁移过程还挺顺利的。先把一条最核心的支付路径用 Playwright 重写,跑通以后对比原来的 Cypress 用例,发现代码量少了差不多三分之一,等待相关的 flaky 问题也明显减少。后面我就把新用例全部写在 Playwright 里,老用例用兼容模式慢慢迁。这里提醒一句:别想着一次大迁移,E2E 是罗盘不是螺丝刀,动一点就要稳一点,小步快跑才是正道。
3. 从零搭建一套能跑的 E2E 工程:目录、配置与第一批用例
不管你选哪个框架,"怎么把第一个用例稳定跑起来"永远是第一步。这个环节最大的坑不是语法,而是工程感——目录怎么分、配置文件怎么写、用例间数据怎么隔离,一开始没想清楚,后面越写越乱。
3.1 初始化与目录设计
我以 Playwright 为例,初始化可以说简单到令人发指:
npm init playwright@latest它会问你几个问题,包括是否安装浏览器、是否生成 GitHub Actions 的 CI 配置、是否生成示例测试。我建议第一次就先留着示例,跑一遍看看目录结构再说。初始化的工程默认长这样:
e2e/ ├── tests/ │ ├── example.spec.ts │ └── login.spec.ts ├── pages/ │ ├── login.page.ts │ └── checkout.page.ts ├── fixtures/ │ └── test.fixture.ts ├── data/ │ ├── users.json │ └── seed.sql └── playwright.config.ts目录的核心思想是把"用例"和"实现细节"分离:tests里只写场景,比如"用户能用正确账号登录";pages里封装页面元素定位和操作,这样页面改版时只需要改一处;fixtures是扩展测试基座的地方,用来注入自定义装置;data单独放测试数据的种子和生产方案的快照。
3.2 配置文件的那些细节
playwright.config.ts是整个工程的心脏,我列出几个必须认真对待的配置项:
import { defineConfig, devices } from '@playwright/test'; export default defineConfig({ testDir: './tests', fullyParallel: true, retries: process.env.CI ? 2 : 0, workers: process.env.CI ? 4 : undefined, reporter: [['html'], ['list']], use: { baseURL: process.env.E2E_BASE_URL || 'http://localhost:5173', trace: 'on-first-retry', screenshot: 'only-on-failure', video: 'retain-on-failure', locale: 'zh-CN', }, projects: [ { name: 'chromium', use: { ...devices['Desktop Chrome'] }, }, ], });几个容易被忽略的地方:baseURL用环境变量覆盖而不是写死,保证同一套用例能跑 dev、staging 和 prod;trace建议设成on-first-retry,既有现场可查又不会让报告撑爆;video和screenshot只在失败时留档,节省 CI 磁盘空间。fullyParallel和workers是性能和稳定性之间的平衡键,parallel 开太大会让测试互相踩数据,开太小又跑太久,这个在后面稳定性的章节细讲。
3.3 写第一个跟业务有关的用例:登录与断言
初始化之后,不要急着写业务用例,先用一条真实场景的登录用例练手。下面这段就是最标准的 POM(Page Object Model)写法的例子:
import { test, expect } from '@playwright/test'; import { LoginPage } from '../pages/login.page'; import { testUser } from '../data/users.json'; test.describe('登录模块', () => { test('用户能用正确账号登录并跳转工作台', async ({ page }) => { const loginPage = new LoginPage(page); await loginPage.goto(); await loginPage.login(testUser.username, testUser.password); await expect(page).toHaveURL(/\/dashboard/); await expect(page.getByRole('heading', { name: '工作台' })).toBeVisible(); }); });对应的LoginPage封装:
export class LoginPage { constructor(private readonly page: Page) {} async goto() { await this.page.goto('/login'); } async login(username: string, password: string) { await this.page.getByLabel('用户名').fill(username); await this.page.getByLabel('密码').fill(password); await this.page.getByRole('button', { name: '登录' }).click(); } }这套写法的价值在于:等哪天登录按钮从"登录"改成了"立即进入",你只需要改LoginPage里的那一个 locator,所有调用的用例都不用动。断言层用expect的自动轮询机制,它会等到元素出现或 URL 变化,不要自己写waitForTimeout——固定 sleep 是 E2E 世界里第一号不稳定因子。
3.4 跑起来以后立刻要做的三件事
第一个用例能稳定跑三次以上以后,别急着欢呼,先把这三件事做了:第一,把 trace 打开,跑一个故意失败的用例看回放,确保失败现场是可查的;第二,在 CI 里跑一遍试试,很多本地通过的用例进了 CI 就开始随机挂了,早发现早处理;第三,把录制的能力交给团队成员试用,让大家先写出几个冒烟路径,培养手感。
这个阶段最容易犯的毛病是过度设计。我见过有人在第一个用例里就引入了几十个抽象类和各种泛型工具函数,结果用例还没跑通,框架先写了一堆 bug。E2E 的工程结构是长出来的,不是设计出来的。一开始保持朴素,等出现真实的重复,再抽页面对象也不迟。
4. profile 档案机制:把"一套用例跑多个环境"从口号变成现实
前阵子在一个项目维护群里看到有人抛出 "e2e profile1" 这个词,其实说的就是在 e2e 工程里用 profile(配置档案)来区分不同的测试场景和运行环境。这个概念非常值得展开。很多人把测试环境只理解成"换个 baseURL",但真正落地的时候你会发现,环境切换牵扯的远远不止一个 URL:测试数据不同、用户权限不同、超时时间不同、要不要 mock 外部服务也不同。
4.1 先搞清楚 profile 到底管理什么
我理解的 profile,是把"用例运行时的上下文"打包成一只开箱即用的盒子。它至少包含四个维度的内容:第一是环境信息,比如域名、接口网关、CDN 版本;第二是数据信息,比如一组可用的测试账号、数据库种子数据、mock 开关;第三是行为信息,比如超时阈值、是否需要跳过某类用例;第四是角色权限,也就是用游客身份跑,还是用管理员、普通会员、被封禁账号来跑。
我习惯的命名规则非常简单粗暴:profile1就是最基础的那套档案——默认环境加默认测试账号,不做任何 mock;profile2开始接入角色的变化,比如管理员操作相关用例;profile3引入 mock 数据,用于不稳定外网依赖的测试。如果后续还有profile4、profile5,一定是档案之间出现了一个新的"重要维度差异",而不是因为无聊。
4.2 用 Playwright 的 project 机制实现多 profile
Playwright 里最妙的设计就是projects,它相当于把整个测试套件按 profile 拆成群组。每个 profile 可以有自己的testMatch、use配置和依赖关系。下面是我的一套简化配置:
import { defineConfig, devices } from '@playwright/test'; export default defineConfig({ projects: [ { // profile1:默认游客态,覆盖冒烟路径 name: 'profile1-smoke', testMatch: /smoke?.*\.spec\.ts/, use: { ...devices['Desktop Chrome'], baseURL: process.env.PROFILE1_BASE_URL || 'https://staging.example.com', storageState: 'state/guest.json', viewport: { width: 1280, height: 720 }, }, }, { // profile2:管理员后台,覆盖权限类路径 name: 'profile2-admin', testMatch: /admin?.*\.spec\.ts/, dependencies: ['setup:admin'], use: { ...devices['Desktop Chrome'], storageState: 'state/admin.json', }, }, { // profile3:mock 网络,覆盖失败兜底类场景 name: 'profile3-mocked', testMatch: /mock?.*\.spec\.ts/, use: { ...devices['Desktop Chrome'], serviceWorkers: 'block', }, }, ], });storageState是最容易被低估的能力。它允许你把登录后的 cookie 和 localStorage 状态导出成一个 JSON 文件,跑用例的时候直接载入,这样就省去了每个用例都重新登录的耗时。配合setup类型的 project,你可以在跑测试之前先自动登录一次,把 token 存进状态文件,然后所有用例共享这个状态,一石二鸟地解决了"登录慢"和"登录态隔离"两个问题。
4.3 用环境变量和 dotenv 组合出真实的 profile
只靠一个配置文件不叫档案管理,真正落地还得接上环境变量。我用的是很传统的方案:根目录建一组.env.profile1、.env.profile2之类的文件,每个文件存该档案下的所有变量:
# .env.profile1 E2E_BASE_URL=https://staging.example.com E2E_USERNAME=playwright_guest E2E_PASSWORD=testpass123 E2E_USE_MOCK=false E2E_TIMEOUT=30000运行的时候用dotenv-cli指定加载哪个文件,比如:
npx dotenv -e .env.profile1 playwright test好处是配置跟代码分离,非工程师的同事也能直接改档案文件,不用碰代码。这里有一个我踩过的坑:环境变量一定要做成"强校验"。曾经因为某个人在 profile 文件里多加了一个空格,整个 CI 里所有的 mock 开关都失效,测试在本地过、在 CI 上随机挂,折腾了两天才找到问题。后来我在加载配置的地方加了一个 schema 校验工具,任何多余或缺失的变量都会直接报错,从那以后这类问题彻底消失。
4.4 profile 机制下的用例组织建议
档案建好之后,用例的组织方式也要跟着调整。我的原则是:tests目录下按功能模块建子目录,每个模块里的用例文件名都要带上它属于哪个 profile 的标记,比如login.spec.profile1.ts、invoice.spec.profile3.ts。这样可以一眼看出"这个场景用的是哪套配置",排查问题时不用来回翻。
另一个重要的点是每个 profile 里都必须有完整的最小冒烟集。也就是说,就算profile2是针对管理员的,它也应该包含"从登录到打开管理后台"这条最小路径,而不是只跑到管理员特有的功能。因为你换了一个 profile 之后,用户角色和权限变了,很多基础功能其实是被隐式影响的,不跑一遍根本不知道。
5. 稳定运行的最后一公里:等待策略、数据隔离与 CI 接入的实战坑
E2E 最劝退人的地方就是不稳定:昨天还好好的,今天没改任何代码,莫名其妙就红了。我花在这上面的时间,比写用例的时间多得多。下面这些都是我拿无数次深夜修用例换来的教训,希望能让你少挨几顿折磨。
5.1 告别 sleep,学会正确的等待姿势
新手写 E2E 最常见的问题就是一遇到元素没出现就sleep(2000),这大概是 E2E 里最糟糕的写法。因为它无论元素出没出现都会等够 2 秒,用例跑得又慢又不稳定,而且一旦网络慢一点,2 秒等不到还是照样失败。
Playwright 的自动等待其实已经内置了最优解:执行点击、填写、断言之前,它会自动等待元素稳定、可见、可操作。你要做的不是再补一层等待,而是管好自己写进去的等待。很多随机失败其实是"气场不对"——比如你等了某个按钮,但这个按钮是 disabled 的,等再久也不会让你点;又比如你等了某个 loader 消失,但实际上 loader 超时了。正确做法是尽量用expect的轮询断言去等状态,而不是等时间:
// 错误示范 await page.waitForTimeout(3000); expect(await page.getByText('加载完成').count()).toBeGreaterThan(0); // 正确示范 await expect(page.getByText('加载完成')).toBeVisible();第二个版本里的expect会自动重试直到超时,元素一旦出现立即通过,快的时候几十毫秒就完事。这才是真正稳定的等待。
5.2 数据隔离:让每条用例活在自己的世界
E2E 反模式排行榜第一名,就是测试之间共享数据。举个例子:注册用例里创建了一个用户叫test01,下一条用例恰好依赖这个用户的数据,如果注册用例因为网络问题慢了几秒,下一条用例就跟着失败。这种级联失败会让人崩溃,而且极其难定位。
我的解法是:每个用例必须拥有独立的数据,不依赖执行顺序。具体做法有三个层次:第一,凡是能通过 API 直接创建的测试数据,就在用例开头用 request 接口建好,而不是通过 UI 去操作;第二,给测试数据加上随机后缀或者时间戳,比如user_1678293841,避免撞车;第三,复杂数据依赖用"前置装置"(fixture)注入,比如在 fixture 里造好订单和库存,再进入用例。
这里还有一个细节很多人不注意:玩过测试数据库的都知道,千万不能让用例依赖数据库里已经存在的历史数据。数据越少,测试越稳定。最好的状态是每条用例启动之前,自己的数据从一张白纸开始。
5.3 CI 上跑的跟本地完全不是一回事
你一定遇到过这种情况:本地怎么跑怎么过,一上 CI 就随机红。原因其实不玄学——CI 环境是全新的,没有你那台电脑里的各种缓存和历史状态;CI 机器资源更少,页面加载更慢,渲染时间更长;CI 上跑的是 headless 浏览器,有些依赖 GPU 或者特殊字体渲染的页面会表现异常;另外 CI 大概率开启了并行 worker,多个用例同时跑,点数派发和数据隔离一旦没做好,就很容易互相干扰。
我的实战经验是把下面几条写进硬性规范:CI 里workers不要拉满,给个 3 到 4 就行,并行太快是随机失败的头号温床;retries设成 1 到 2 次,允许偶发网络抖动,但超过两次还失败就说明真有问题了;CI 机器的时区、语言、字体全部固定,用一个统一的docker镜像跑,我在镜像里甚至把中文字体都预装好了,就为了截图的时候不出现方块字。
还有一条非常实际的建议:跑完 CI 以后,务必要在报告里保留完整 trace 和截图。没有现场证据的失败报告等于没有报告,你会被迫一次又一次地无脑重跑,浪费时间。trace 里的网络瀑布图和页面快照能直接指向问题所在。
5.4 headless 模式下的那些小怪异
headless 模式下最容易踩到的坑是:页面动画不会正常结束,或者某些元素在 headless 里不可见。有个项目,页面上的 loading 效果用的是 WebGL,在 headless 环境下渲染不出来,导致expect(loader).toBeHidden()一直失败。最后只能在配置里对特定用例禁用 WebGL 特性,或者用--disable-gpu参数统一处理。
另一个常见问题是布局影响点击坐标。Playwright 默认用元素中心点点击,如果页面在 headless 下因为字体差异导致了布局偏移,有些元素会被遮挡住,点击就会失效。解决办法只有一个:定位器尽量用语义化定位器(getByRole、getByLabel),不要用坐标偏移操作。依赖坐标的用例几乎等于一颗定时炸弹,页面任何布局调整都会炸到你头上。
6. E2E 用例的"精力管理":哪些值得自动化,哪些写了就是负资产
最后这部分,我想聊聊很多技术文章不太会明说的话题:E2E 用例不是越多越好。一个团队每天的维护精力就那么多,如果这个精力被大量低价值用例吃掉,真正该保住的业务核心反而会失守。
6.1 高价值用例的清单:核心旅程优先
我内部有个简单的判断标准:一条用例值不值得写,取决于三个问题的答案。第一,这条路断了,用户会不会直接骂娘?第二,这条路跨了几个模块,单测能否覆盖到?第三,这条路径是不是每周都在变?如果三个问题里至少有两个是肯定的,那它值得写。如果三个都是否定的,我建议你把它从计划表里划掉。
在我的项目里,高价值清单永远是这几类:注册和登录全流程、支付全流程(包括支付失败、取消支付)、核心链路的状态流转(比如订单从待支付到已发货的所有状态)、权限边界(比如非会员访问会员专享页面必须被拦)、数据导入导出类功能。这五类只要没有严重回归问题,业务就基本不会崩。
6.2 低价值用例的黑名单:写它就是害自己
反过来,下面这几类我强烈建议不要写:文案和视觉细节的断言,比如说某个按钮文案必须是"立即下单"——这种断言每次营销改词都会红,没有任何回归价值;一次性活动页面的用例,活动下线后用例就成了尸体;跟外部第三方系统深度耦合的流程,除非你能稳定 mock 第三方,否则你每次都在赌对方的系统不出问题;高频变化的 UI 组件库内部的样式级改动,那是组件库自己单测的事,不是 E2E 的事。
还有个特别容易被忽视的坑:给"错误状态"写用例时,别把异常路径的边界拉得太宽。比如验证某个表单要写同一个字段的几十种非法输入,这是单元测试该干的事。E2E 只需要验证"最典型的错误提示能出现"就够了,剩下的交给下面两层,不然测试套件会膨胀到没人敢改代码。
6.3 给用例定期"体检":失败率、维护频率和丢弃机制
E2E 套件跟人一样,要定期体检。我每两周会看一眼所有用例的统计:整体运行时长是多少、失败率前五名是谁、最近有没有用例被高频修改。如果一条用例连续一个月都出现在失败率前五,而且每次失败原因都是定位器选择器变了,那大概率不是用例的问题,而是它本来就不该存在。
我习惯给每条用例打一个"健康分",权重是:运行时长占 30%,最近一个月的失败次数占 40%,被修改的次数占 30%。低于某个分数的用例,我不会急着删除,而是先在项目里标注为@skip一周。如果一周内没有业务方跑来找它,就说明它已经死了,直接删除,不需要留恋。这招看起来很粗暴,却能有效阻止 E2E 套件在无人关注的情况下一路膨胀到没法维护。
6.4 长期维护的节奏感
还有一个心法层面的东西想分享:E2E 不是一次性投入,它更像是整个团队对"质量底线"的集体习惯。你不需要每周都加一堆新用例,但每周都应该花一点时间看它绿不绿、有没有随机失败、有没有新功能产生了新的核心路径。测试代码和业务代码一样需要 code review,需要重构,需要有"主人"。没有主人的 E2E 项目早晚会烂尾。
我个人现在的节奏是:功能开发时决不写 E2E,等这个功能上线稳定两周,确认不会被回炉重造,再回来补核心路径。很多团队把 E2E 当成开发的最后一步,催着同事"功能写完顺便把用例写了",结果功能还在改,用例就得跟着改,越改越乱,最后直接放弃。让 E2E 晚半拍进场,看起来是慢,实际上是最快的路径。
7. 最后再分享一点我的真实体会
如果让我总结做 e2e 这几年最重要的领悟,大概就是:端到端测试不是工具问题,也不是技术问题,它首先是取舍问题。你得承认它的昂贵,然后谨慎地把它用在最值钱的地方;你得接受它会有偶发失败,然后构建好排查机制,而不是一红就慌;你得让整个团队都理解"这条用例在保护什么",否则再漂亮的测试工程也会在忙碌中被悄悄废弃。
还有一个很实际的小建议送给你:不管你的团队有多忙,请一定在最早阶段就把 trace 和截图机制打开。我遇到过太多次线上事故,当时唯一能还原现场的就是 CI 里留下的那个带着所有请求记录的 trace 文件,它比任何日志都有说服力。哪天你会感谢当初花十分钟打开这个开关的自己。