ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

端到端测试实战:从用例设计到稳定性治理的完整指南

端到端测试实战:从用例设计到稳定性治理的完整指南

端到端测试这东西,在软件测试圈里一直是个争议最大的角色。爱它的人说它是上线前最后一道保险,恨它的人说它慢、贵、动不动就崩。我这些年从刚开始写自动化测试到维护十几条核心业务的 E2E 用例,态度变了好几次,但最后还是比较坚定的:对于业务链路长的系统,E2E 是绕不开的一道闸门。它最核心的价值不是帮你发现多少 bug,而是帮你验证“每个模块单独看都没问题,串起来走一遍业务流程到底通不通”。很多系统就是挂在“连接”上出的问题,接口单独调正常,页面单独渲染正常,但用户从登录到下单一路走下来,中间某一个异步回调没触发,整条链路就断了。

这篇文章不聊虚的,我会把 E2E 测试从设计思路、工具选型、配置文件管理,到实际编写用例、跑 CI、排查不稳定问题的完整过程拆开讲一遍。特别是“e2e profile”这个概念,很多人会忽略它在测试环境隔离和登录态管理里起的作用,我会重点展开。无论你是刚接触自动化测试的新人,还是已经被 E2E 稳定性折磨过的老手,这篇都能给你一些可以直接用的思路。

1. 端到端测试的核心思路与整体设计

1.1 E2E 测试到底解决的是什么问题

很多人把 E2E 测试理解为“模拟用户操作”的测试,这个说法对,但还不够本质。E2E 测试真正验证的是系统各层之间的契约是否成立,包括前端与后端接口的契约、服务与数据库的契约、主流程与第三方服务的契约。它跟单元测试和集成测试有一个本质区别:单元测试验证的是函数行为,集成测试验证的是模块协作,E2E 验证的是业务价值的交付。

举个例子,一个用户注册流程。单元测试会验证邮箱格式校验函数、验证码生成逻辑、用户信息写入数据库的方法。集成测试会验证调用注册接口时,参数传递、HTTP 响应、数据库字段映射是否正常。但这两个层面都覆盖不了一个问题:用户在前端页面输入完信息,点下注册按钮之后,整个系统从浏览器事件、网络请求、服务端逻辑、数据库持久化到页面跳转这整条链路跑下来,会不会因为某个细节失败?

比如我踩过的一个很典型的坑:前端某个按钮点击后同时触发两个接口请求,后端并行处理这两个请求,其中一个接口依赖另一个接口的返回值,但前端的 Promise 没有正确串行,导致偶发的时序错乱。单元测试测不出这种问题,集成测试只测其中一个接口也发现不了,必须在真实环境里完整走一遍用户操作路径才能暴露。这就是 E2E 测试不可替代的原因。

1.2 E2E 测试在测试金字塔里的位置

说 E2E 之前,先把它放回测试金字塔里看。经典的测试金字塔分三层,底层是大量的单元测试,中层是适量的集成测试,顶层是少量的端到端测试。这个金字塔的比例是有道理的:单元测试运行快、成本低、定位精确,适合覆盖大量边界条件和分支逻辑;集成测试则聚焦模块之间交互的契约;E2E 测试成本最高、运行最慢,但每个用例覆盖的代码路径也最完整。

E2E 测试不是用来替代前两层的,而是用来补位。它真正负责的是“跨模块的完整业务场景”,所以我一直建议:如果你团队的 UI 自动化用例已经多到跑一次需要一两个小时,反过来要去压减单元测试的投入,这个方向就是错了。E2E 应该保持精简,一条用例覆盖一条核心业务链路,而不是事无巨细地去点点点。

从覆盖层面看,测试金字塔每往上一层,测试对真实用户行为的模拟程度就更进一步,但对应的维护成本和执行时间也随之水涨船高。所以我的经验是:E2E 用例数量宁可少而精,也不要多而滥。一个常见的误区是团队为了覆盖率这个 KPI,把大量重复的页面跳转步骤拆成十几个用例,结果每次前端稍微动一下布局,十几个用例一起红,维护成本直接爆炸。

1.3 什么样的项目真正需要引入 E2E

不是所有项目都适合着急上 E2E。如果你的项目是一个纯展示型页面,或者业务逻辑非常简单,只有一两张表单,那 E2E 的投入产出比就很低,用集成测试把接口层覆盖好可能更实际。但如果你的项目有这些特征,E2E 就值得认真考虑:

  • 核心业务链路长:比如电商的下单支付流程、金融产品的开户流程、后台管理系统的权限审批流程,用户要走七八步才能完成一个业务闭环,这种场景非常适合 E2E。
  • 多系统交互频繁:比如前端系统要对接多个内部服务,或者要跟第三方系统交互,任何一方的接口变动都可能引发链路崩溃,E2E 用真实环境做端到端验证最接近线上的真实情况。
  • 频繁回归需求强烈:如果产品每个迭代都会改动核心路径上的代码,手工回归一遍成本又很高,这时候自动化 E2E 的价值就特别明显。

另外我要提醒一点:E2E 测试启动的时机不能太晚。最怕的就是项目长期不写 E2E,等系统复杂到一定程度再想补,测试数据准备、环境搭建、定位器维护的成本都会变得非常高。比较合理的方式是在核心链路稳定后的第一个迭代周期就开始搭,后续每个迭代都跟着维护。

2. 工具选型与测试配置设计

2.1 主流的 E2E 测试工具怎么选

E2E 测试工具这几年迭代很快,我最早用的是 Selenium WebDriver,后来用过一段时间的 Nightwatch,再后来在 Cypress 和 Playwright 之间纠结了很久。现在我做新项目基本上一律推荐 Playwright,但不妨碍先说说这几个工具的差异。

Selenium 的历史地位不用多说,它最大的优势是生态老、支持的浏览器和语言多,几乎所有主流语言都能写。但它的问题也很明显:WebDriver 协议是底层的、浏览器驱动的模式比较笨重,写复杂断言和等待策略的时候代码量很大,稳定性也依赖驱动的版本管理。如果你是在一个遗留技术栈上维护老项目,Selenium 的兼容性确实是它还能存活的核心原因。

Cypress 在前几年火过一阵,它的优点是对开发者友好,自带交互式调试工具,时间旅行调试体验确实好。但它的架构决定了它有两条硬伤:一是它运行在浏览器内,跨域访问和多个标签页的场景支持得比较别扭;二是不支持多浏览器并发,至少在很长一段时间内不支持。如果你的系统需要同时验证 Chrome、Firefox、WebKit,Cypress 会有些吃力。

Playwright 是我目前的主力选择。它由微软团队维护,底层通过 CDP(Chrome DevTools Protocol)与浏览器交互,支持的浏览器包括 Chromium、Firefox、WebKit,而且支持多浏览器并行执行。它的自动等待机制做得非常聪明,绝大多数情况下你不需要手写sleep或者waitForTimeout,它会自动等待元素可交互。另外它的 trace viewer 功能对排查失败用例很有帮助,可以完整回放测试运行过程中的页面状态、网络请求和控制台日志。

工具选型没有绝对的对错,关键看你当前的团队背景、技术栈和项目形态。但有一条建议可以给:选工具的时候把你最痛的那个场景列出来,用试用 demo 实际跑一遍,别只看文档和博客。

2.2 e2e profile 这个配置到底怎么玩

现在来聊一个容易被忽略但特别实用的点:E2E 中的 profile 配置管理。这个术语在不同的项目里含义不完全一样,我理解的是“针对不同测试环境、不同测试角色、不同运行场景的一组配置集合”。

很多团队的 E2E 测试脚本写完了,但换一套测试环境就跑不通,或者换一台机器就神秘失败。这种问题十有七八出在配置文件上。比如你的测试环境有 dev、staging、生产环境,前端域名不同、后端接口地址不同、测试账号密码不同、第三方 mock 的开关也不一样。这些配置如果散落在测试代码里用全局变量到处引用,维护起来就是一场灾难。

比较好的做法是建一套 profile 机制,每个环境对应一个独立配置块,运行测试时按配置项加载。我在实际项目中一般这样组织:

{ "baseUrl": { "dev": "https://dev.example.com", "staging": "https://staging.example.com" }, "apiBaseUrl": { "dev": "https://api-dev.example.com", "staging": "https://api-staging.example.com" }, "userProfiles": { "admin": { "username": "e2e_admin", "password": "e2e_password_admin" }, "regular": { "username": "e2e_user", "password": "e2e_password_user" } }, "userProfile1": { "username": "profile1_user", "password": "profile1_password" } }

这里的userProfile1就是我在实际项目里很喜欢用的一种 profile 形式:为测试场景定制专属用户。为什么要单独搞一个profile1用户?核心原因是数据隔离。如果所有测试都用同一个 admin 账号,并行执行的时候一个用例改了用户配置,另一个用例可能就受影响了。用不同的 profile 用户去执行不同场景的用例,可以在很大程度上避免测试之间的数据互相干扰。

在代码里加载 profile 的方式也很灵活,我一般用环境变量来控制当前跑的是哪套环境,配合配置文件读取:

E2E_ENV=staging USER_PROFILE=profile1 npx playwright test

命令行里传入的 profile 决定测试中登录哪个账号、断言哪个环境的主机地址。这样做的好处是:测试代码本身不需要做任何逻辑判断,配置切换全部在外面做。从 CI 到本地,通过参数就可以无缝切换环境,不需要改代码、不需要重新编译,测试用例的复用性也会提高不少。

2.3 测试环境与测试数据隔离

聊到 profile,就躲不开环境隔离和测试数据隔离的问题。E2E 测试最怕的就是测试数据互相污染。比如你有一条用例是验证“用户创建订单后能看到订单列表”,如果前面的用例已经往这个用户下面塞了一堆历史订单,后面的用例断言订单数量就会失败。

处理这个问题,核心思路有两个方向:一个是每个用例都准备独立数据,另一个是执行完用例之后做数据清理。两者各有优劣。

独立数据准备的方式比较稳定,但成本相对较高。比如注册账号这种操作,每次跑用例都现注册一个随机账号,数据肯定独立,但注册过程本身又会增加用例执行时间。我现在比较推荐的做法是结合 profile 机制做一个“数据池”:预先把一批测试账号分成几个 profile 用户,每个 profile 对应一类业务场景,数据在测试开始前用接口预埋好。

数据清理则要注意一个原则:不要在用例的断言步骤前清理,最好在用例执行完之后清理。我会在每个测试模块的 afterAll 钩子里调用清理接口,把这轮测试产生的数据标记删除或者直接物理删除。但这里有个坑要提醒你:清理操作本身如果写得不对,反而会把别的用例正在用的数据删了。所以数据清理必须严格限定范围,比如清理只针对测试账号创建的数据,带测试标记的数据才删除。

3. 完整实操:从用例设计到跑通一条 E2E

3.1 先设计用例,再写代码

很多 E2E 写不好,根子不在于代码能力,而在于用例设计阶段就出了问题。我见过太多人打开 IDE 就开始写click、fill,写到哪算哪,结果断言逻辑不清晰,用例执行起来吵翻天,既不知道覆盖了什么业务,也不知道失败意味着什么。

用例设计阶段,我一般会让测试工程师和业务、开发一起过一遍核心用户旅程,把最重要的 5 到 10 条用户路径画出来。比如对一个电商平台来说,核心路径大概是这些:

  • 未登录用户搜索商品并加入购物车,然后引导登录
  • 登录用户从加购到提交订单,再到支付成功
  • 已支付订单在订单列表中的状态流转
  • 用户申请退款,商家审批退款的完整流程
  • 后台管理员修改商品库存,前台库存数实时变化

每一条路径拆出来,明确四件事:前置条件、操作步骤、预期结果、异常分支。前置条件对应前面说的 profile 和环境准备,预期结果对应断言,异常分支决定这条用例本身是否值得自动化。

用例设计完成后,再落到 Playwright 里。这里我强烈建议一个做法:用 describe 组织业务模块,用 test 组织具体场景,但每个 test 只做一条完整链路,不要把一个业务流程拆成多个 test。拆碎测试用例会导致每次跑用例都重复登录重复准备数据,执行效率反而下降。

3.2 用 Playwright 快速搭建第一个用例

这里以 Playwright 为例,直接上代码。假设我们要验证一个典型的登录加购流程:

import { test, expect } from '@playwright/test'; import { loadProfile } from './profile-loader'; // 从配置文件加载当前运行环境对应的 profile const profile = loadProfile(process.env.USER_PROFILE || 'userProfile1'); const baseUrl = profile.baseUrl; test.describe('电商核心下单链路', () => { test.beforeEach(async ({ page }) => { // 每次用例之前都干净地打开首页 await page.goto(`${baseUrl}/`); }); test('用户可以从搜索到下单走通全流程', async ({ page }) => { // 搜索商品 await page.getByPlaceholder('输入商品名称').fill('无线键盘'); await page.getByRole('button', { name: '搜索' }).click(); // 点击第一个商品进入详情 await page.locator('.product-list-item').first().click(); // 加入购物车 await page.getByRole('button', { name: '加入购物车' }).click(); // 进入购物车并提交订单 await page.goto(`${baseUrl}/cart`); await page.getByRole('button', { name: '去结算' }).click(); // 这里会跳转登录页,用 profile 里的用户信息登录 await page.getByPlaceholder('用户名').fill(profile.userProfiles.regular.username); await page.getByPlaceholder('密码').fill(profile.userProfiles.regular.password); await page.getByRole('button', { name: '登录' }).click(); // 填收货地址、提交订单 await page.getByPlaceholder('收货地址').fill('测试地址 1 号'); await page.getByRole('button', { name: '提交订单' }).click(); // 断言:订单提交成功页面出现 await expect(page).toHaveURL(/order-success/); await expect(page.locator('.order-number')).toBeVisible(); }); });

注意我这里的几个写法细节,都是实践中攒的经验:

第一,定位器优先用getByRole和getByPlaceholder,而不是 CSS 类名直接查。因为类名是前端最容易改的,今天叫.product-list-item,明天前端重构类名就变了,用例直接挂。getByRole从语义化角度定位元素,跟用户实际感知更接近,前端改类名的概率远远低于改语义结构。

第二,断言尽量用自动等待的expect结合toBeVisible或toHaveURL,不要自己写waitForTimeout(3000)。固定等待是最差劲的做法,网络一波动用例就废,自动等待让断言在条件满足时才继续执行,既稳定又节省时间。

第三,用例内部不要写一堆 if 分支。E2E 用例的目的是验证一条确定性的路径,如果你在用例内部搞各种条件判断,测试结果就会变得不确定,失败了你都不知道是前置条件不对还是被测系统出了问题。

3.3 登录态管理和用户 profile 的注入

任何真实项目的 E2E 都绕不开登录态。如果每个用例都从登录页开始输账号密码,不仅执行时间长,而且把登录流程这个低频场景重复覆盖了很多遍。比较理想的做法是:登录流程单独测一个用例,其他用例直接复用登录态。

Playwright 提供了一个特别实用的机制,storageState。第一次登录成功之后,把浏览器上下文状态保存到文件,后续用例直接复用,几秒种就能进入登录完成状态。结合前面的 profile,可以做这样一个逻辑:

import { test, expect } from '@playwright/test'; test('保存登录态到 profile 文件', async ({ browser }) => { const context = await browser.newContext(); const page = await context.newPage(); await page.goto(`${baseUrl}/login`); await page.getByPlaceholder('用户名').fill(profile.userProfiles.regular.username); await page.getByPlaceholder('密码').fill(profile.userProfiles.regular.password); await page.getByRole('button', { name: '登录' }).click(); await expect(page.locator('.user-center')).toBeVisible(); await context.storageState({ path: `profiles/${profile.name}-storage.json` }); await context.close(); });

后续的用例在创建 context 的时候,直接注入这个存储状态:

test.use({ storageState: 'profiles/userProfile1-storage.json' });

这个方案我用了很久,稳定性一直很高。有个细节要单独提一下:保存的 storageState 里包含的 cookies 和 localStorage 是有时效性的,特别是 session 过期机制,一旦过期所有复用该状态的用例都会跟着失败。所以我的解决方案是维护一个 token/Cookie 刷新任务,在 CI 跑之前先执行一次登录态刷新用例,然后再跑主测试集。

profile1 这个命名在登录态场景里也很适用。你可以为“购物车用户”“有历史订单用户”“新注册用户”各建一个 profile 登录态文件,跑不同场景时自动选择对应 profile,这比每次动态注册用户快得多。

3.4 在 CI 里跑 E2E 的配置思路

本地跑能过、CI 跑就挂,是 E2E 最常见的翻车现场。本地和 CI 的环境差异不小,核心差异在浏览器依赖、内核能力、并行策略和网络环境。

先解决浏览器依赖。Playwright 跑之前需要下载对应版本的浏览器,本地可能早就装好了,CI 是全新环境就要单独拉取。在 CI 脚本里,启动任务的第一步就要把浏览器装好:

npx playwright install --with-deps

这个命令会同时把系统依赖一起安装,避免缺系统库导致的诡异 crash。我踩过一次坑:CI 里没装系统依赖,跑起来一多半用例都报browserType.launch失败,日志里写的错误是 segment 相关,根本看不出来是缺系统库,排查了很久。

再一个重点是并行策略。Playwright 默认可以多 worker 并行跑,但 CI 机器资源有限,并行开太高会导致 CPU 被打满,用例反而更容易超时。比较稳妥的做法是在playwright.config.ts里根据 CI 机器配置限制并发:

import { defineConfig } from '@playwright/test'; export default defineConfig({ timeout: 60000, workers: process.env.CI ? 2 : undefined, retries: process.env.CI ? 2 : 0, reporter: process.env.CI ? [['list'], ['json', { outputFile: 'e2e-results.json' }]] : 'list', });

这里的retries策略要分开说。我建议 CI 上可以设置 1 到 2 次重试,主要为了解决偶发网络抖动造成的失败。但重试不是银弹,如果一个用例每次都在同一位置挂掉,重试只会掩盖真正的问题。所以我会同时开 trace 和截图,每条失败用例都保留现场证据,再配合日志里的失败报告定位。

说到网络环境,还有一个小坑:CI 里跑 E2E 的时候,如果被测系统的后端依赖外网服务,或者第三方接口不稳定,往往会造成偶发失败。这个我一般会用 Playwright 的page.route()把第三方依赖 mock 掉,只保留业务系统本身的真实性。E2E 测试验证的是你自家系统的链路,没有必要每次跑都去依赖第三方。

4. 常见问题与排查技巧实录

4.1 E2E 测试的“不稳定”到底是怎么来的

E2E 测试很多人一开始很兴奋,写了二三十条用例,过了一个月发现每次跑都有几条随机失败,于是开始质疑自动化测试的价值。其实大部分不稳定问题都不是测试工具本身造成的,而是设计层面的问题。

不稳定主要来自这几个方面:测试环境不稳定、测试数据污染、网络波动、定位器脆弱、时序依赖。我整理了一张速查表:

问题现象大概率原因解决思路
用例偶发在同一个步骤超时等待策略失效,定位器不稳定换语义化定位器,检查自动等待条件
执行结果依赖用例执行顺序测试数据未隔离,存在状态共享引入 profile 用户隔离,或者独立数据准备
本地通过,CI 必挂浏览器依赖缺失或系统库不完整用playwright install --with-deps完整安装
某个用例三分之一的概率失败接口时序或异步渲染导致用expect自动等待代替固定 sleep
登录态失效导致大量用例失败登录 token 过期加登录态刷新机制,在测试套件前置阶段统一处理
页面元素存在但点击无反应有遮罩层或元素被遮挡用click({ force: true })前先检查遮挡来源

排查不稳定问题,我的经验是先拿 trace 和截图说话,不要靠猜。Playwright 的 trace viewer 能看到每一步操作的 DOM 快照,还能看到网络请求瀑布,这可比翻代码快多了。

4.2 定位器写不好,E2E 就没法稳定

定位器是 E2E 测试的根基。测试用例 80% 的代码都在做元素定位和交互,定位器写得不好,后面所有断言都是空中楼阁。我整理了几条定位器的心得,基本属于“早踩坑早改”的内容:

  • 别用xpath一撸到底。XPath 定位能力强,但耦合度太高,前端结构一变你的定位器就断。能用getByRole、getByLabel、getByText就别用 XPath。
  • 不要用page.locator('div.page > div.item > div.name')这种超长路径。这种定位器对前端 DOM 结构变化极其敏感。尽量用靠近业务语义的定位方式。
  • 留意动态 id 和随机 class。有些前端框架会生成类似_ngcontent-abc123这种动态属性,这类属性绝对不能用来定位。
  • 同一页面有多个类似元素的时候,用filter配合hasText精确筛选。比如一个列表页有多行商品,可以用page.locator('.product-row').filter({ hasText: '无线键盘' })来精确定位目标行,避免取到第一行或者随机行。

有一点我特别想强调:定位器的选择要站在“用户怎么感知”的角度去想。用户看不到类名,看到的是一个写着“搜索”的按钮。所以getByRole('button', { name: '搜索' })就是在模拟真实用户的感知方式,这类定位器在页面样式层重做的时候往往也不容易失效。

4.3 测试数据污染是最大的隐性杀手

前面零星提到过数据污染,这里集中展开。E2E 跑久了最隐蔽的问题就是数据污染。明明是同一个用例,昨天跑得好好的,今天跑就失败,排查半天发现是页面上多了一条昨天测试留下的脏数据。

数据污染的三个主要来源:

  • 用例失败中断后没清理,下次跑的时候脏数据还在。
  • 多个用例共享同一个账号,一个用例改了用户资料,另一个用例就断在“期望用户名为 default”这种断言上。
  • 时间驱动的数据,比如“近 30 天订单统计”这种查询,隔几天跑一次结果都不一样。

我的解决方案是组合拳:

第一,每个 profile 用户只服务于一条测试链路,用例之间不要共享账号。profile1 就是购物车流程专用,profile2 可以做订单管理专属,这样并行执行也互不干扰。

第二,用例执行完后主动清理测试产生的外部数据。清理要设计在用例的 finally 钩子里,不管用例成功失败都执行,而且清理动作本身用 API 直连而不是 UI 逐条删除,否则清理过程比测试本体还要慢。

第三,测试中对涉及时间的逻辑统一注入时间源。如果被测系统支持 mock 时钟,就尽量用;不支持的话,在设计用例的时候避开依赖具体日期的断言,用相对时间窗口替代。

4.4 执行速度慢的问题怎么破

E2E 慢是共识,但慢到跑完要一个多小时,那一定要做减法。我见过太多团队 E2E 用例数量失控,从最初计划的十来个涨到一百多个,每次发布等结果等到天荒地老。

控制执行时间有几个手段:

  • 减少重复的前置操作。复用登录态 profile 是最立竿见影的方案,一次登录态保存能省下每条用例 5 到 10 秒的登录时间。如果 50 条用例里有 40 条需要登录,这个优化就是立竿见影的。
  • 并行执行但限制并发总量。开太多 worker 会互相抢占资源,比如都用同一个浏览器的不同进程,系统资源不够用时反而跑得更慢。找到适合自己机器的 worker 数,通常 CI 机器 2 到 4 个 worker 效果最好。
  • 划分 smoke 集和完整集。核心关键链路划到 smoke 集,每次提交都跑;完整的 E2E 集放在主分支合并后或者每天定时跑。这样既保证频繁发布的场景有快速反馈,又能保留完整回归的能力。

还有一点很多人没注意到:E2E 用例的断言数量不是越多越好。断言太多,每一处都在等待页面状态,执行时间会显著拉长。一条用例只断言核心业务结果,次要逻辑交给集成测试和单元测试去覆盖,这样 E2E 的执行效率才能保持在一个健康的水平。

5. 我的一些实操心得收尾

做 E2E 测试这五年多,我最深的体会是:E2E 不是一个技术问题,是一个工程管理问题。很多团队技术选型选得很好,Playwright 也用了,CI 也接上了,但最后还是放弃了,原因往往在于用例维护成本和业务迭代速度之间没有找到平衡。

我自己走过的弯路可以列出来给大家提个醒。刚开始做 E2E 的时候,我特别追求覆盖率,觉得用例写得越多越有安全感,结果每两周要花一两天时间修用例,修完又出新的问题,陷入疲于奔命的循环。后来把用例收敛到核心用户路径,放弃那些低频、低价值的页面流程,情况才慢慢好转。敢于删用例比敢于写用例更需要判断力。

另外一个体会是关于 profile 的使用。我见过很多团队在 E2E 里把账号密码硬编码在脚本里,各环境的地址散落得到处都是,最后环境一变更,所有人都在改代码。用统一的 profile 配置把所有环境差异隔离出去,是 E2E 走向规范化管理的关键一步。这看起来是个小改动,但带来的维护体验提升非常可观。

最后再分享一个小技巧。E2E 用例失败的时候,光看报错信息往往不够,我习惯让每个失败的用例自动附带一份当时页面的完整状态:截图、浏览器 console 日志、网络请求日志、DOM 快照。把这些信息打包上传到 CI 的 artifact 里。这样每次失败我都不是对着屏幕猜,而是打开 trace 文件直接看回放,定位问题的速度快了一倍不止。这个习惯,我一直保留到现在,也是我面对任何 E2E 稳定性问题时的第一招。

返回列表