1. 项目概述与E2E的核心价值
1.1 这个项目到底在做什么
“e2e”这三个字母,在测试圈里几乎每天都有人提。它全称是End-to-End,端到端测试,简单说就是从真实用户的视角,把一条完整的业务链路从头到尾跑通,验证整个系统在真实环境下能不能正常协作。比如一个电商系统,用户注册、登录、搜索商品、加购物车、下单、支付、收订单通知——这条链路任何一个环节挂了,用户感知到的就是“这个网站不能用”。
我们这次梳理的“e2e profile”相关工程实践,核心就是围绕端到端测试的配置化与场景矩阵化管理展开的。很多人以为E2E测试就是写几个自动化脚本点点页面,实际落地时远没那么简单:测试环境怎么切换?浏览器矩阵怎么配?登录态怎么在不同用例间复用?这些零零碎碎的问题,最后都会汇聚到一个词上——profile,也就是测试配置档案。
我在一线做测试开发和质量保障有十年了,从早期的Selenium脚本一路摸到Playwright和Cypress时代。说实话,端到端测试是投入产出比最微妙的一层:写好了,它是整个回归体系的定海神针;写砸了,就是CI里天天红、人人都不想碰的定时炸弹。这篇文章会把E2E项目的整体设计思路、profile配置的核心逻辑、从零搭项目的实操流程,以及我这些年踩过的坑一次说清楚。
1.2 端到端测试解决什么问题
先搞清楚一个容易被误解的点:接口测试、单元测试、E2E测试,它们不是替代关系,而是互相兜底的关系。
单元测试锁的是函数级别的最小行为,接口测试锁的是服务之间的数据契约,但这两者都回答不了一个最核心的问题——用户点击那个橙色按钮之后,整个系统到底能不能让他顺利完成操作?数据库连接池这会儿会不会崩?前端异步请求的时序对不对?第三方支付回调有没有绕回来?这些跨服务、跨层的协作问题,只有在真实浏览器里跑一遍完整流程才能被发现。
我见过太多项目,接口测试覆盖率90%以上,一到上线还是出问题。原因很简单:接口测的是“每个零件都合格”,E2E测的是“整台机器跑起来不散架”。尤其是涉及登录态流转、前后端联调、微服务网关路由这类场景,没有E2E做兜底,上线靠的就是运气。
那E2E适合谁来搞?如果你是一个后端开发,写好接口之后想确认前端真能调到;如果你是一个前端开发,重构页面后担心改崩了原有交互;如果你是一个测试工程师,想搭建一套可靠的自动化回归防线——这篇文章都适用。不需要你有很强的编程基础,但建议至少会用一种语言写脚本,Node.js或Python都行。
2. 技术选型与Profile配置的核心逻辑
2.1 主流E2E框架怎么选
做E2E测试,第一步不是写用例,而是选工具。工具选错,后面每一步都别扭。这几年主流方案无非三套:Selenium、Cypress、Playwright。我直接给一张对比表,都是我实际用下来的感受。
| 维度 | Selenium WebDriver | Cypress | Playwright |
|---|---|---|---|
| 架构模式 | 客户端-服务端,通过WebDriver协议控制浏览器 | 在浏览器内运行,与页面同进程 | 基于CDP协议,独立进程控制浏览器 |
| 语言支持 | Java/Python/JS等全语言 | JavaScript/TypeScript | JavaScript/TypeScript/Python/Java/.NET |
| 多浏览器 | Chrome/Firefox/Safari/Edge全覆盖 | 主打Chrome系,Firefox和WebKit支持有限 | Chromium/Firefox/WebKit全部官方支持 |
| 多标签页/多窗口 | 支持但API老旧,处理麻烦 | 不直接支持多标签页 | 原生支持,Context隔离很干净 |
| 自动等待 | 基本靠显式等待,写起来啰嗦 | 内置自动等待,DOM变化感知好 | 内置自动等待,等待策略最激进也最稳 |
| 网络拦截与Mock | 能力弱,需要代理工具配合 | 支持cy.intercept,拦截HTTP | 支持page.route,可拦截HTTP/WebSocket |
| 安装复杂度 | 需要额外管理driver版本 | npm安装即用,自带浏览器管理 | npm安装后npx playwright install即可 |
| 调试体验 | 截图+日志,比较原始 | Time Travel回放,调试体验最好 | Trace Viewer录制全链路,带网络明细和DOM快照 |
选型建议很简单:如果团队是Java技术栈,历史包袱重,Selenium还能用;如果团队认准JavaScript/TypeScript,且希望测试跑得快、写起来顺手、跨浏览器覆盖轻松,我基本都推荐Playwright。Cypress的调试体验确实好,但多标签页和WebKit支持这两个硬伤,在某些业务场景下会非常别扭。
我们这个项目最终选了Playwright,原因后面会细讲,但最有决定性的一点是——Playwright的profile能力天然强。它把“不同浏览器、不同环境、不同用户角色”的配置矩阵做到了框架层级,这正好对上了我们做E2E工程化的核心诉求。
2.2 Playwright里的“Profile”到底是什么
很多人看到“e2e profile”这个词会懵:profile到底指什么?在Playwright里,它通常指配置文件(playwright.config.ts)中定义的一组可运行单元,官方叫project。一个project可以绑定自己的浏览器、基础URL、设备模拟参数、登录态、测试文件匹配规则等等。
为什么要搞profile?举个最典型的例子:一个后台管理系统,运营人员用的是Chrome,管理员偶尔用Safari;用户端页面要测移动端视口下的交互。如果没有profile机制,你要么把测试跑好几遍手动切参数,要么写一堆环境判断if-else把配置文件搞得一团糟。有了project profile,这些都变成声明式配置了。
看下面这段配置,这是Playwright里最基础的profile拆解:
// playwright.config.ts import { defineConfig, devices } from '@playwright/test'; export default defineConfig({ testDir: './tests', // 测试用例目录 timeout: 30_000, // 单用例超时时间 retries: 2, // CI环境下失败重试次数 workers: 4, // 并行worker数 use: { baseURL: 'https://staging.example.com', // 默认环境 trace: 'retain-on-failure', // 失败时保留trace }, projects: [ // profile 1: Chrome端,跑桌面Web的核心回归 { name: 'e2e-profile1-chrome', use: { browserName: 'chromium', viewport: { width: 1440, height: 900 }, ...devices['Desktop Chrome'], }, testMatch: /.*\.spec\.ts/, }, // profile 2: 移动端Web,跑H5核心流程 { name: 'e2e-mobile-h5', testMatch: /.*mobile.*\.spec\.ts/, use: { browserName: 'chromium', ...devices['iPhone 13'], }, }, ], });这个例子里其实出现了两个profile:一个是桌面Chrome端的完整回归profile,另一个是移动端H5的专用profile。它们共享同一套测试代码,但运行时用的浏览器参数、视口尺寸、匹配的测试文件都不同。这就是profile的根本价值——一份用例,多份配置,按场景自由组合出测试矩阵。
2.3 多Profile矩阵的三种典型场景
我在实际项目里总结下来,profile矩阵最常见的有三类应用场景。搞清楚这三类,你就能根据自己项目的需要做取舍了。
第一类是浏览器兼容矩阵。产品要求支持Chrome和Firefox,那最简单就是定义两个project,分别绑不同浏览器。Playwright跑的时候会把同一批用例在两种浏览器各执行一遍,一个用例崩在Chrome上不代表Firefox也崩,分开报告、分开定位。这种场景适合对浏览器兼容性有硬指标的项目,比如面向公众的官网、政务类系统。
第二类是多环境切换。一个测试环境、一个预发布环境、可能还有本地环境。不同环境的域名、账号、权限配置不一样。如果这些信息硬编码在用例里,换个环境就得改代码,抽到配置里又容易因为漏改导致误测。用profile把“环境”这个维度拆出来就会非常干净:
// 按环境拆分的profile projects: [ { name: 'staging', use: { baseURL: 'https://staging.example.com' }, grep: '@smoke', }, { name: 'production-smoke', use: { baseURL: 'https://example.com' }, grep: '@smoke', }, ]第三类是用户角色与登录态隔离。后台系统往往有超级管理员、普通运营、只读访客等角色。不同角色看到的菜单、按钮、可操作范围完全不同。如果把所有用例混在一起跑,登录态切换就是灾难。更合理的做法是为每个角色单独做一个profile,配合storageState把登录态固化下来,用例里不再反复走登录流程。
projects: [ { name: 'admin-profile', use: { storageState: './auth/admin.json', }, testMatch: /.*admin.*\.spec\.ts/, }, { name: 'operator-profile', use: { storageState: './auth/operator.json', }, testMatch: /.*operator.*\.spec\.ts/, }, ]这个写法背后的逻辑是:把身份认证的开销前置,通过一次登录把cookie和localStorage快照存到JSON文件里,后续每个用例启动浏览器时直接注入这个快照,省去重复的登录步骤。这不仅快,而且稳定——不会因为登录接口偶尔抖动导致一堆用例跟着挂。
3. 实操过程与核心环节实现
3.1 初始化一个E2E项目要准备什么
选型定了之后,就进入实操环节。这里我把整个初始化流程完整走一遍,你可以直接照着抄。
第一步是准备Node.js环境。我推荐Node 18 LTS或更高版本,主要是Playwright对高版本的Node支持更稳,npm安装依赖时也不容易碰上peerDependencies冲突。
第二步是初始化项目和安装依赖:
mkdir e2e-project cd e2e-project npm init -y npm install -D @playwright/test npx playwright install这里几个命令各自干了什么,我拆开说一下。npm init -y生成一个默认的package.json;npm install -D @playwright/test安装测试框架本体;npx playwright install下载浏览器二进制文件,包括Chromium、Firefox和WebKit三套。下载体积不小,如果网络不好容易中断,中断后重新执行npx playwright install即可,它是断点续传的。
第三步是验证安装是否正常。你可以先跑一下npx playwright test,此时没有用例,会提示No tests found,这其实是正常的,说明框架本身能起来。如果这一步就报错,多半是浏览器下载不完整,重新执行安装命令就好。
项目结构上,我建议按下面这种方式组织,虽然初期看起来目录多,但项目大了你会感谢自己:
e2e-project/ ├── tests/ # 测试用例目录 │ ├── admin/ # 管理端用例 │ ├── mobile/ # 移动端用例 │ └── api/ # 需要调用接口的场景 ├── fixtures/ # 自定义fixture(测试夹具) ├── utils/ # 公共方法 ├── auth/ # 登录态JSON存放处 ├── playwright.config.ts # 总配置文件 └── .env # 环境变量3.2 配置文件的逐项拆解
配置文件是E2E项目里最值得花时间打磨的部分。我见过太多项目,用例本身写得不错,但配置一塌糊涂:超时时间设得太短导致偶发失败、没开trace导致失败后定位全靠猜、baseURL写死在用例里导致换环境就得全局替换。这些都是配置没想清楚的典型症状。
这里给出一份比较完整的配置模板,每项参数后面跟着我的说明:
import { defineConfig, devices } from '@playwright/test'; export default defineConfig({ // 测试用例入口目录 testDir: './tests', // 每个用例的执行超时时间,不是整个文件的总超时 timeout: 30_000, // expect断言轮询的超时时间,定位元素时最常见的就是这个 expect: { timeout: 5_000 }, // 失败重试次数:本地建议0,CI建议2 // 重试的意义是规避网络抖动、第三方服务不稳定等偶发因素 retries: process.env.CI ? 2 : 0, // 并行worker数:本地推荐等于CPU核心数减1 // CI上建议按机器性能和资源配额调整 workers: process.env.CI ? 4 : undefined, // 全局共享配置,会被project里的use局部覆盖 use: { baseURL: process.env.BASE_URL || 'https://staging.example.com', headless: true, viewport: { width: 1280, height: 720 }, ignoreHTTPSErrors: true, trace: 'retain-on-failure', video: 'retain-on-failure', screenshot: 'only-on-failure', }, // 配置两个项目的profile projects: [ { name: 'e2e-profile1-chromium', use: { browserName: 'chromium' }, testIgnore: /.*mobile.*\.spec\.ts/, }, { name: 'e2e-mobile-safari', use: { browserName: 'webkit', ...devices['iPhone 13'] }, testMatch: /.*mobile.*\.spec\.ts/, }, ], });几个容易踩坑的点:
timeout和expect.timeout是两个东西。前者管整个用例的生命周期,后者管每次断言的轮询等待。如果你定位元素很快但整个用例总超时,问题出在timeout;如果你页面加载很快但某个元素迟迟不出现,问题出在expect.timeout。trace一定要开。retain-on-failure的意思是只有用例失败时才保存trace文件,这个文件可以用npx playwright show-trace打开,里面能看到每一步操作的DOM快照、网络请求和浏览器console日志。实测下来,80%的失败单靠trace就能定位,省去了反复复现的功夫。video和screenshot按需开,全开的话CI产物会非常大,而且对失败的排查来说,一张失败瞬间的截图远不如一段完整trace管用。
3.3 编写第一个端到端用例
配置就绪后,写用例其实是最直白的一步。Playwright的用例风格和Cypress很像,都是BDD式的describe/test嵌套。下面用一个“用户登录并完成商品搜索”的场景做示例:
import { test, expect } from '@playwright/test'; test.describe('登录与搜索流程', () => { test('用户使用合法账号登录后可以搜索商品', async ({ page }) => { // 1. 访问登录页 await page.goto('/login'); await expect(page).toHaveTitle(/登录/); // 2. 填写账号密码 await page.getByLabel('用户名').fill('test_user_001'); await page.getByLabel('密码').fill('password123456'); // 3. 提交登录 await page.getByRole('button', { name: '登 录' }).click(); // 4. 断言登录成功的标志——用户昵称出现在页面右上角 await expect(page.getByText('test_user_001')).toBeVisible(); // 5. 搜索商品 await page.getByPlaceholder('搜索商品名称').fill('无线耳机'); await page.keyboard.press('Enter'); // 6. 等待搜索结果列表加载,断言第一条结果包含搜索词 const firstResult = page.locator('.product-card').first(); await expect(firstResult).toContainText('无线耳机'); }); });这套代码里值得展开讲的有两点。
第一是选择器策略。我强烈建议优先使用getByRole、getByLabel、getByPlaceholder这些基于可访问性属性的选择器,而不是page.locator('.product-card')这种依赖class的选择器。前者的特点是贴近真实用户理解页面元素的方式,页面class一改,后者立刻稀疏,前者几乎不受影响。用WAI-ARIA属性做定位策略,其实是在为测试代码建立一道“防重构”的缓冲。
第二是自动等待机制。Playwright几乎所有操作API都会自动等待目标元素可交互,比如click()会先检查元素是否存在、是否可见、是否稳定(没有被动画干扰),再执行点击。这一点和Selenium时代“交给我,我不保证”的思路完全不同,也是现代E2E框架大幅降低测试脆弱性的根本原因。
3.4 测试数据管理与登录态复用
E2E测试最折磨人的问题之一就是数据。你想验证“下单成功后订单列表出现新订单”,下单接口每次产生的订单号都不同,列表断言怎么写?你想验证“用户修改昵称后昵称更新”,但测试连跑了三遍之后,昵称已经变成了“test123”、“test123-2”、“test123-3”,断言写死就挂了。
我建议的通用解法是把测试数据分成三类处理:
第一类是接口造数。在用例执行前,通过API调用直接把测试数据铺好,不走UI操作。比如要测订单列表,就先用API创建一笔已知订单,再进页面断言它的展示状态。这比在UI上一路操作下单快得多,而且可控。Playwright里可以通过requestfixture直接发HTTP请求,不需要单独引axios。
test('订单列表展示已支付订单', async ({ page, request }) => { // 通过API创建一笔订单 const resp = await request.post('/api/orders', { data: { productId: 'sku_10001', amount: 19900, payMethod: 'alipay', }, }); expect(resp.ok()).toBeTruthy(); const order = await resp.json(); // 进页面断言订单可见 await page.goto('/orders'); await expect(page.locator(`text=${order.orderNo}`)).toBeVisible(); });第二类是独立数据池。每个用户角色维护一组专用测试账号,账号之间数据互不干扰。这里有个细节:测试账号的密码和权限不要在生产环境生效,所有测试账号必须在测试环境独立维护,避免安全风险。
第三类是清理机制。在afterEach或全局teardown里清理数据。比如执行完用例后调用删除接口,把刚才创建的订单标记为已取消或直接删除。清理可能比创建更麻烦,但不清,数据只会越堆越多,最终某天跑用例时被历史脏数据误伤。
至于登录态复用,实操里最省心的是用Playwright官方的storageState方式。你先写一个setup用例,专门用来登录并保存状态:
// tests/auth.setup.ts import { test as setup, expect } from '@playwright/test'; import path from 'path'; const authFile = path.join(__dirname, '../auth/user.json'); setup('登录并保存认证状态', async ({ page }) => { await page.goto('/login'); await page.getByLabel('用户名').fill('test_user_001'); await page.getByLabel('密码').fill('password123456'); await page.getByRole('button', { name: '登 录' }).click(); await expect(page.getByText('test_user_001')).toBeVisible(); await page.context().storageState({ path: authFile }); });然后在配置文件的对应profile里声明storageState: './auth/user.json',之后所有归属这个profile的用例,启动时浏览器都已经处于登录状态,直接从业务页面开始测就行。一条setup用例,全profile收益,这是E2E工程的效率杠杆。
3.5 运行、报告与CI集成
用例写好了,跑起来的方式也讲究。Playwright命令行默认执行整个testDir下的所有用例,但在实际项目里,你往往只想知道某个profile或某条用例跑得怎么样。
# 跑全部 npx playwright test # 只跑指定project(profile) npx playwright test --project=e2e-profile1-chromium # 只跑带某个tag的用例 npx playwright test --grep @smoke # 最后失败重跑,快速定位 npx playwright test --last-failed其中最常用的是--project,对应我们前面说的profile。比如改动涉及移动端页面时,只跑移动端profile,十几秒出结果,不用等桌面端全部跑完。这个用法我几乎每天都要用。
报告方面,npx playwright test结束后可以在终端看到逐条用例的PASS/FAIL摘要,但真正排查问题时还是要打开HTML报告:
npx playwright show-report报告里每个用例都有执行时间、网络请求瀑布图、控制台报错、以及失败时的trace入口。我用下来的体会是:排查一个失败用例,先开trace,再看console报错,最后才看业务日志,这个顺序最高效。
最后是CI集成。不管用GitLab CI还是Jenkins还是GitHub Actions,核心步骤就三条:装依赖、装浏览器、跑测试。以GitLab CI为例,一个最小化的配置长这样:
image: mcr.microsoft.com/playwright:v1.45.0 stages: - test e2e: stage: test script: - npm ci - npx playwright install --with-deps - npx playwright test --project=e2e-profile1-chromium artifacts: when: always paths: - playwright-report/ - test-results/这里有个关键点:用Playwright官方发布的Docker镜像,浏览器和系统依赖已经预装好了,省去了很多折腾。另外建议把playwright-report/和test-results/设为CI产物,这样失败时可以直接下载报告分析,不用登录CI机器翻日志。
4. 常见问题与排查技巧实录
4.1 元素定位失败与超时问题
问题现象:用例跑到中间,报locator.click: Timeout 30000ms exceeded,定位的元素在页面上死活找不到。
排查思路:先判断元素是真的不存在,还是存在但不可交互。如果页面有CSS动画、loading遮罩、弹窗遮挡,都可能让元素“技术上存在但无法点击”。
Playwright处理这个问题的思路是:点击前会先滚动到元素、等待元素稳定、再执行点击。如果元素被其他元素遮挡,会明确告诉你哪个元素挡了路。这里最有效的排查工具是——失败前先手动打开页面看一眼,如果页面有loading过场动画,多半是动画时序没处理好。
我的常用解法是在操作前加一步显式等待:
// 等待某个条件先满足,再继续往下走 await page.waitForSelector('.product-list.loaded'); await page.getByRole('button', { name: '加入购物车' }).click();不过要警惕:waitForSelector不是越多越好。加得越多,用例执行越慢,而且会产生一种“依赖等待来碰运气”的坏味道。更健康的方式是从根上设计好页面状态的标识——比如数据加载完成后给列表加一个loadedclass,这比在客户端反复试等待策略靠谱得多。
4.2 “本地能过,CI挂”怎么解
这是E2E项目里最经典的灵异事件。本地跑得好好的,一上CI就各种超时、元素找不到、断言失败。我排查这类问题,第一步永远不是看代码,而是对比环境。
CI和本地最大的区别通常三个:网络速度、机器性能、浏览器启动方式。CI机器性能较弱时,页面渲染慢,元素出现的时机比本地晚几百毫秒甚至几秒。如果你的用例里大量依赖隐式等待,本地感觉不到,CI就原形毕露。
我的应对办法是把“等待策略”的阈值整体上调,而不是给每个用例加sleep。回到配置里,把expect.timeout从5秒调到10秒,timeout从30秒调到60秒。同时把CI判断条件从“基于是否设置了某些环境变量”改成“基于运行环境是否带CI标记”,前者容易漏配,后者一般平台都会注入。
还有一个容易忽略的点:CI上浏览器默认以headless模式运行,而headless模式下的渲染行为、字体加载、动画帧率和有头模式并不完全一致。以前遇到过某个页面在headless模式下字体渲染异常导致按钮宽度变化、出现遮挡的情况,这类问题只能靠加trace和分析截图来发现。
4.3 偶发性失败与重试策略的平衡
E2E测试最让人头疼的不是“必挂”,而是“时好时坏”。一会绿一会红,没有任何代码改动,push一下它挂了,re-run一遍又过了。这类偶发失败,业内叫flakey test,如果数据量大,会直接消磨团队对测试结果的信任。
我的经验是把偶发失败分成两类:一类是外部依赖不稳,比如第三方登录接口偶尔返回500、短信验证码服务暂时不可用;另一类是测试自身问题,比如断言了不该断言的文案、使用了不稳定的选择器。
对于前者,合理的使用重试机制是必要的。CI上配置retries: 2,相当于每条用例最多跑三遍,前两遍挂了还有最后一层机会。但重试不是万灵药——如果你发现某条用例频繁消耗重试机会,说明它本身质量不过关,应该标记为flaky然后单独排查,而不是让整个CI陪它一起反复横跳。
对于后者,治本的办法是重构用例。尤其是“断言具体文案”这个行为要非常克制,因为产品文案改动频繁,前脚写死“立即购买”,后脚产品就改成“马上抢”,用例立刻跟着挂。更稳的做法是断言页面状态和可交互元素,而不是具体的人类语言。
4.4 Profile配置中的常见坑
配置profile时最常见的低级错误是storageState路径写错或文件不存在导致浏览器启动即失败。这个错误会在跑所有用例前先给你当头一棒,提示信息是Error: ENOENT: no such file or directory。解决思路是检查setup用例是否成功执行过,确认路径和名称大小写完全一致。
第二个高频坑是profile之间互相污染。比如你在一个profile里改了全局的baseURL,其他profile也跟着变了,这种“A改B挂”的问题排查起来非常耗时。我的习惯是:profile的use配置里绝不放全局配置,全局配置只放所有profile共享的部分,每个profile只覆盖自己独有的差异。同时在命名上带上业务含义,比如admin-smoke、mobile-regression,不要叫profile1、profile2这种看不出用途的代号。
第三个坑是关于testMatch和testIgnore的。如果你一个文件既匹配了testMatch又被另一个project的testIgnore排除,Playwright可能会报文件重复归属或找不到归属的警告。解法很简单:每个用例文件只能且必须归属一个project,在设计目录结构时就想清楚这个文件归哪个profile,不要让多个profile去抢同一个文件。
4.5 调试时的几个实用小技巧
最后分享几个日常调试中出效率的小技巧,这些是我这些年攒下来的私货。
第一,用--debug模式。npx playwright test --debug会打开一个调试面板,每一步操作都会暂停,可以查看当前DOM、网络请求、浏览器存储,鼠标悬停还能看到选择器对应的元素高亮。这个模式对初学者理解框架执行过程极有帮助。
第二,用page.pause()。在用例代码里插入await page.pause();,执行到这一行时测试会暂停,这时候你可以手动操作浏览器,再继续执行后面的代码。我经常用这个方法处理“我判断不清楚这一步步操作会发生什么”的场景。
第三,善用Trace Viewer的“Action”时间线。打开trace后,左侧是每一步操作的列表,点任意一步,右侧会呈现当时的页面快照和网络请求。从这个视图里能看到一个用例在执行过程中经历了哪些页面状态变化,很多元素定位失败的根因就藏在某一步操作前后的DOM差异里。
5. 写在最后的实操体会
E2E测试工程做到后面,你会发现一个真相:它本质上不是写脚本问题,而是系统设计问题。你选择哪些场景做E2E、如何切割profile矩阵、怎么管理测试数据和登录态、怎么在CI里调度执行,这些决策对项目质量的影响,远大于你用了哪个定位器API。
我在实际项目里的一个体会是,E2E用例应该“少而精”。很多人一上来就恨不得把全站每个按钮都做成自动化用例,结果维护成本直线上升,最后整个套件变成无人敢动的“遗产”。成熟的团队通常只把核心链路、高价值链路拿出来做E2E,比如登录注册、下单支付、权限控制、关键数据展示,其余的部分交给接口测试和单测去覆盖。
最后分享一个小技巧:每个profile的用例队列里,把“冒烟级”用例(标记@smoke)放在执行顺序的前面,这样一旦环境有基础性问题,几分钟内就能暴露,而不用等全量用例跑完才发现环境挂了。我们在CI里单独拆了一个冒烟流水线,每次发布前先跑冒烟级profile,全绿后才跑完整回归。这个习惯帮团队省下了无数个小时的等待时间,也避免了很多次“全员围观CI转圈”的尴尬。