☰
端到端测试与e2e profile实战:从Playwright选型到稳定性优化
2026/10/26 7:17:19 网站建设 项目流程

1. 先从“e2e”这个缩写聊起

如果你在软件行业待过一段时间,肯定见过“e2e”这个词,随手一搜,热搜词里也总能看到“e2e”“e2e profile1”的影子。很多人一开始都会懵一下:e2e 是啥?是 end to end 的缩写,也就是“端到端”。但在不同语境下,它代表的含义其实差得挺远。

对测试工程师来说,e2e 指的是端到端测试——模拟真实用户从打开页面、登录、操作业务、到提交数据的完整流程;对系统架构师来说,e2e 可能意味着端到端加密、端到端链路追踪;对产品经理来说,它可能是一句话:“这条业务路径要端到端跑通”。

这篇内容,我主要从软件测试和工程质量的角度来拆解 e2e。而我特别想聊的一个话题,是最近行业里讨论度越来越高的e2e profile——它不是一个单独的测试工具,也不是某个框架的插件,而是把端到端测试按“运行场景”拆分成不同档案(Profile)来做精细化管理的一套思路。说白了,就是同一个产品,在不同环境、不同用户角色、不同业务模式下,跑的 e2e 用例集合和参数配置可以是完全不一样的,把这些差异固化成一份份配置档案,就是 e2e profile 要解决的事。

这篇内容适合谁看?我觉得三类人最需要:一是刚接触自动化测试、想搞明白端到端测试到底怎么落地的测试新人;二是负责搭建测试体系、正在为“e2e 跑得太慢、太不稳定”发愁的测试开发工程师;三是产品研发团队里想理解“为什么端到端测试这么重要,但又这么难伺候”的开发或项目经理。

我把我自己在多个项目中落地 e2e 测试的真实经验和踩坑记录都放在后面了,有些配置是直接可以抄作业的,有些思路则需要你根据自己的业务场景去调整。咱们一步步来。

2. e2e 测试为什么是“最后一道防线”,也是最难伺候的一道

2.1 先分层:单元、集成、端到端到底测什么

在聊 e2e 之前,有必要回顾一下测试金字塔的概念。经典的分层是这样的:

层级测试对象典型工具运行速度稳定性
单元测试函数、类、模块内部逻辑JUnit、pytest、Jest极快(毫秒级)高
集成测试模块之间、服务之间的交互Testcontainers、Spring Boot Test较快(秒级)中
端到端测试完整的用户业务链路Playwright、Cypress、Selenium慢(分钟级)低

单元测试保证的是“零件没坏”,集成测试保证的是“齿轮咬合没问题”,而 e2e 测试保证的是“整条流水线转起来,最终用户能拿到合格产品”。

打个比方:你买一辆车,单元测试是检查发动机每个气缸的压缩比,集成测试是验证变速箱和发动机能不能匹配,e2e 测试则是你坐进驾驶座,点火、挂挡、踩油门,真正把车开上马路跑一圈。你说哪个更重要?都重要,但只有“开一圈”才能发现座椅角度不对、后视镜视野被遮挡、刹车踏板太远这种真实使用才会暴露的问题。

前端项目里 e2e 尤其重要,因为浏览器里发生的事情太多:异步请求的顺序、接口返回的时机、DOM 渲染的节奏、用户交互的反馈,这些东西单测和集成测试都很难完全模拟。e2e 直接站在用户视角跑真实场景,这是它不可替代的根本原因。

2.2 为什么 e2e 这么“难伺候”

我接触过的团队里,普遍对 e2e 有两种极端态度:一种是“不做,因为太不稳定,跑了也白跑”;另一种是“什么都想塞进 e2e,恨不得全站核心功能都用浏览器自动点一遍”。这两种其实都走偏了。

先说“稳定”这个问题。e2e 不稳定有很多来源:

  • 时序问题:前端操作后接口返回有快有慢,按钮还没可点击就点了,于是失败。
  • 环境问题:测试环境数据被其他人改了、服务重启了、依赖的第三方接口波动了。
  • 选择器问题:前端重构后类名变了、DOM 层级调整了,原来定位元素的路径就失效了。
  • 并发问题:两个测试同时跑,互相抢数据、抢资源,结果随机失败。

这些问题每一个单独看都不算难解决,但叠在一起就让人头大。这也是为什么很多团队跑了几次 e2e 之后,草草放弃——因为没人愿意每天都在修那些“昨天还绿今天红”的用例。

但我个人的观点是:e2e 难,不代表它不该做,而是要用对方法做。关键就在于“克制”二字——不是所有功能都需要 e2e,但核心业务链路必须有 e2e;不是所有环境都要跑全量 e2e,而是要用 profile 的思路把用例分门别类。

2.3 e2e profile:一套用例,多种跑法

这里我重点说说 e2e profile 这个概念。我们团队在维护几十个端到端用例之后,发现了一个问题:所有用例塞在一起跑,一次全量执行要四十分钟,而且经常因为某几个用例在特定环境下不稳定,导致整条流水线被阻塞。后来我们做了拆分,按“档案”来管理:

  • smoke profile:只跑最核心的冒烟路径,比如登录、首页加载、创建订单,10 分钟以内跑完,每次部署后必跑。
  • regression profile:覆盖全量核心业务链路,包括各种边界分支,每天晚上定时跑,或者发版前跑。
  • permission profile:专门跑不同角色的权限路径,比如普通用户、管理员、访客,每种角色用独立账号和独立数据。
  • mobile profile:针对移动端视口和触摸操作的用例集合,单独成档。

每个 profile 就是一份配置文件,里面定义了要包含哪些用例、用哪些环境变量、跑在哪个浏览器、并发数多少、超时时间多长。这样就能做到“按需取用”,而不是每次全量硬跑。

后来我把这种思路更进一步:连测试数据也做成 profile 的一部分。比如“电商下单链路”和“售后退款链路”需要的前提数据完全不同,那就为不同业务域维护不同的数据 profile,跑哪个业务域的 e2e 就加载对应的数据档案。这套做法在团队里磨合下来,效率提升非常明显。

3. 选型:Playwright、Cypress、Selenium 怎么选

3.1 三足鼎立的现状

聊 e2e 实操,工具选型是绕不开的第一步。目前市场上主流的端到端测试工具就是三个:Selenium、Cypress、Playwright。它们的定位和优势差别挺大。

特性SeleniumCypressPlaywright
架构基于 WebDriver 协议,客户端远程驱动浏览器和被测应用同进程运行,直接操作 DOM基于 CDP 协议,通过调试协议控制浏览器
语言支持Java、Python、JS、C# 等几乎全部主流语言仅 JavaScript/TypeScriptJavaScript/TypeScript、Python、Java、.NET
多标签页/多浏览器支持,但配置繁琐支持较弱,早期版本几乎不支持原生支持,非常方便
网络拦截不原生支持支持 cy.intercept支持 page.route,很强大
等待机制基本靠显式等待,写起来繁琐自动等待,比较智能自动等待,配合 actionability 检查
调试体验一般极佳,时间旅行回放非常好,trace viewer 看录屏和网络详情

如果你是 2023 年以后新启动的项目,我个人推荐直接用 Playwright,没有特殊理由不要从零开始选 Selenium。为什么?不是 Selenium 不好,它生态成熟、支持语言多,但它的定位更像是“底层标准”,用起来需要拼装很多周边能力。Cypress 体验确实好,但它对多标签页、多域名这类复杂场景的支持比较弱,而且只能跑在自家 runner 里。

Playwright 的优势在于,它把“浏览器自动化”这件事做得很底层很干净。它直接和浏览器 DevTools 协议对话,所以可以做网络拦截、模拟弱网、拦截请求、修改响应,还能跨浏览器跑(Chromium、Firefox、WebKit),甚至能跑移动端浏览器仿真。这些能力在做 e2e 测试时都是实打实的刚需。

3.2 选型时的真实考量

上面那个表是纸面上的,但实际选型的时候,我们还要看几个额外的维度:

  • 团队技术栈:如果团队主要是 Java 后端,测试人员也更熟悉 Java,那 Selenium + WebDriverManager 可能比 Playwright 的 JS 版本更容易上手。但 Playwright 也提供了 Python 和 Java 绑定,而且 API 设计明显比 Selenium 现代,所以这个理由其实也不够硬。
  • CI 环境支持:Playwright 自带安装浏览器的命令,npx playwright install一把梭,在 Docker 里也有现成的官方镜像可以拉。Selenium 则需要 Selenium Grid 或者独立的容器编排,复杂度完全不在一个量级。
  • 调试定位效率:e2e 测试失败之后,定位问题是最大的时间开销。Playwright 的 trace viewer 能记录整个测试过程的 DOM 快照、网络请求、控制台输出,失败后直接拉一份 trace 出来看,基本不需要重新跑一遍。Cypress 的时间旅行也有类似效果。而 Selenium 失败后通常只有一张截图和一段堆栈,效率差很多。

所以说,选型真不是看谁名气大,而是看你最痛的点在哪里。如果你最痛的是“测试写起来麻烦、跑起来不稳、挂了不好定位”,那 Playwright 和 Cypress 明显优于 Selenium;如果你最痛的是“团队全是 Java 工程师、和 Appium 要共用一套代码”,那 Selenium 可能更贴。

我个人的建议是:前端技术栈的团队直接用 Playwright;如果你们测试团队历史上已经有大量 Selenium 投入,可以继续用,但新写的 e2e 建议逐步迁移到 Playwright。迁移的成本没有想象中那么大,因为测试用例的核心逻辑就是“操作元素 + 断言”,换一套 API 表达而已。

3.3 为什么我最终站 Playwright

我不主张无脑跟风,但 Playwright 确实解决了很多我实际遇到的痛点。举例来说,e2e 测试里有个经典难题:前端上传文件。Selenium 里要处理那个原生的文件选择窗口,得用 AutoIT 或者 Robot 类去模拟按键,又脆弱又恶心。而 Playwright 里就是一句话:page.setInputFiles,直接把本地文件路径传进去就行。再比如,测试里需要验证一个接口是否被调用、调用的参数是什么,Cypress 有cy.intercept,Playwright 有page.route,都能做得很优雅,但 Selenium 想做到这个级别就要借助 BrowserMob 这类代理工具了。

还有一点,Playwright 的自动等待做得非常聪明。它会在执行点击、输入等动作之前,自动检查元素是否“可操作”:可见、稳定、不被遮挡、可点击。这一套 actionability 检查机制,直接把一大部分 flaky 问题消灭在了源头。而 Selenium 里如果你只写了findElement().click(),碰上按钮还没渲染出来,那就是一个 ElementClickInterceptedException 等着你,然后你就得手动加 WebDriverWait,麻烦得很。

4. 从零搭一个可运行的 e2e 项目:以 Playwright 为例

4.1 环境准备与初始化

这部分我是基于常见实践来写的,也是我每次在团队里带着新人搭环境的标准流程。假设你准备在一个全新的前端项目里接入 e2e,第一步就是安装 Node.js 环境,建议直接用 18 以上的 LTS 版本,npm 随附。

接着执行:

npm init -y npm install -D @playwright/test npx playwright install

npx playwright install会下载 Chromium、Firefox、WebKit 三个浏览器内核。这里要注意,下载地址默认在国外 CDN,国内网络环境可能很慢,如果碰到下载超时,可以配置环境变量指向镜像源:

export PLAYWRIGHT_DOWNLOAD_HOST=https://npmmirror.com/mirrors/playwright

然后重新执行npx playwright install就可以了。这一步我几乎每次都要帮同事处理一次,属于典型的高频坑。

初始化完成后,在项目根目录建一个playwright.config.ts,这是整个 e2e 项目的核心配置。我常用的基础配置长这样:

import { defineConfig, devices } from '@playwright/test'; export default defineConfig({ testDir: './tests/e2e', timeout: 30_000, expect: { timeout: 5_000, }, fullyParallel: true, retries: process.env.CI ? 2 : 0, reporter: [ ['list'], ['html', { outputFolder: 'playwright-report', open: 'never' }], ], use: { baseURL: 'https://staging.example.com', trace: 'on-first-retry', screenshot: 'only-on-failure', video: 'retain-on-failure', }, projects: [ { name: 'chromium', use: { ...devices['Desktop Chrome'] }, }, { name: 'firefox', use: { ...devices['Desktop Firefox'] }, }, { name: 'webkit', use: { ...devices['Desktop Safari'] }, }, ], });

这里的testDir指向放测试文件的地方,timeout是每个用例的最大执行时间。retries在 CI 环境里设成 2,意思是失败的用例自动重试两次。有人会觉得重试是掩盖问题,但从实战角度看,e2e 受环境波动影响太大,给一两次重试机会是合理的容错策略,关键是要配合 trace 和报告,把重试后通过的真实原因找出来。

use.trace设为on-first-retry,意思是第一次失败后自动开始录制 trace,方便查问题。video和screenshot也是失败时自动保留,这些在排查 flaky 用例时简直是救命稻草。

4.2 目录结构与管理模式

项目一大,e2e 的用例文件就不能乱放。我用的项目结构是这样的:

tests/ e2e/ auth/ login.spec.ts logout.spec.ts order/ create-order.spec.ts cancel-order.spec.ts common/ helpers.ts constants.ts fixtures/ data/ admin-user.json normal-user.json profiles/ smoke-profile.ts regression-profile.ts utils/ request.ts database.ts

虽然 Playwright 支持把测试配置、辅助函数和用例都放一起,但到后期用例一多,有个清晰的结构比什么都重要。我一般会把辅助函数(比如登录态获取、订单数据构造)抽到common或utils目录里,把测试数据和不同的 profile 配置放到fixtures目录下,这样用例文件本身就能保持短小、可读。

4.3 核心用例怎么设计:登录 → 下单 → 退款全链路

我拿电商系统最常见的链路来举例:用户登录 → 搜索商品 → 加入购物车 → 提交订单 → 付款 → 申请退款。这是我们团队真正在生产环境跑过的用例,逻辑很长,拆开看每一步都有不少细节。

先看登录部分。这里有个基础但非常重要的技巧:不要把登录动作写进每一个用例里。我见过很多测试新手在每个测试文件开头都执行一次 UI 登录操作,结果整个测试套件跑一个小时,其中四十分钟都在登录。Playwright 提供了 storageState 机制,可以先把登录后的 cookie 和 localStorage 保存下来,其他用例直接复用。

import { test, expect } from '@playwright/test'; test('登录成功后保存认证状态', async ({ page }) => { await page.goto('/login'); await page.getByLabel('用户名').fill('tester01'); await page.getByLabel('密码').fill('passw0rd'); await page.getByRole('button', { name: '登录' }).click(); await expect(page).toHaveURL(/\/home/); await page.context().storageState({ path: 'auth/user.json' }); });

之后其他用例在配置里直接引入登录态:

test.use({ storageState: 'auth/user.json' }); test('登录后创建订单', async ({ page }) => { await page.goto('/products/10001'); await page.getByRole('button', { name: '加入购物车' }).click(); await page.getByRole('button', { name: '去结算' }).click(); // ... });

这个做法把 e2e 的执行时间缩短了一大截。但也要注意一个副作用:如果登录态过期时间是 30 分钟,而你的用例跑了 40 分钟,那么后半段用例会因为 token 过期而失败。所以对超长套件,需要考虑在用例执行中途“刷新登录态”,或者拆分成更小的 profile 来跑。

再看搜索、加购、下单链路。这一段的重点是学会用 getByRole、getByLabel 这类“语义化”选择器,而不是死死盯着 CSS 类名。比如:

await page.getByRole('textbox', { name: '搜索关键词' }).fill('无线耳机'); await page.keyboard.press('Enter'); await expect(page.getByText('搜索结果')).toBeVisible(); await page.locator('.product-card').filter({ hasText: '无线耳机' }).first().click();

为什么推荐语义化选择器?因为前端的样式类名是最容易被重构改动的,你写死了.product-card,哪天 UI 库升级把类名变成.product-card-v2,你的用例就红了。而 role、label 这类属性通常反映的是用户可感知的语义,变动频率小得多。

提交订单之后,页面一般会跳到收银台,这时前端会调用支付接口,但测试环境里不能真付钱。这时候就要用到 Playwright 的网络拦截能力了:

await page.route('**/api/payment/charge', async (route) => { const response = { code: 0, message: 'mock success', data: { transactionId: 'mock_123' } }; await route.fulfill({ json: response }); });

用page.route把支付接口的响应替换成我们准备好的 mock 数据,这样前端就走通了“支付成功”的分支,而不用真的发起一笔支付。这是 e2e 里非常经典的实践:业务链路要真实,但第三方依赖要可控。

退款链路的核心理念也一样——操作订单详情页的“申请退款”按钮,然后在退款原因下拉框里选一个原因,提交之后断言“退款申请已提交”的状态出现。因为真实退款 API 涉及资金操作,测试环境一定要用 mock 响应替代。

4.4 Profile 拆分在代码里怎么落地

前面说了 e2e profile 的概念,这里我给一个实际可行的落地例子。Playwright 原生支持多个 project,我们可以为不同的 profile 定义不同的 testMatch 和依赖:

projects: [ { name: 'smoke', testMatch: /smoke\/.*\.spec\.ts/, retries: 1, }, { name: 'regression', testMatch: /regression\/.*\.spec\.ts/, fullyParallel: true, }, { name: 'permission', testMatch: /permission\/.*\.spec\.ts/, testIgnore: /.*\/mobile\/.*/, }, ]

你也可以通过环境变量来控制跑哪个 profile:

E2E_PROFILE=smoke npx playwright test

或者在playwright.config.ts里动态读取:

const profile = process.env.E2E_PROFILE || 'regression'; const configs = { smoke: { testMatch: /smoke\/.*\.spec\.ts/ }, regression: { testMatch: /.*/ }, };

这样 CI 和本地都可以通过一个环境变量灵活选择要跑哪些 e2e 用例,而不是傻乎乎地每次都全量跑。从团队管理角度看,不同 profile 对应不同的执行策略和责任人,比如 smoke 档是开发提交代码后的小时级验证,regression 档是发版前的全量回归,permission 档是权限模块改动后专门验证权限边界的。这种按使用场景划分的方式,就是 e2e profile 的核心价值。

5. 数据准备与测试隔离:e2e 稳定运行的地基

5.1 测试数据的三条路:造、借、备份

e2e 测试依赖数据,而数据问题往往是 e2e 不稳定的最大来源。我给团队定的规矩是:绝不直接操作生产数据,也不允许测试用例之间共享同一份业务数据。

常用的数据策略有三类:

  • API 预置数据:在跑测试前,通过调用后端接口创建测试用户、测试商品、测试订单。这是最推荐的方式,因为速度快、可控性强。
  • 数据库直插数据:直接连测试库往表里插入预设行。优点是彻底可控,缺点是一旦表结构变了,脚本就得维护。
  • UI 操作造数:通过界面操作一步步创建需要的数据。最不推荐在 e2e 里做,因为慢而且自身也不稳定。只在确实没有其他接口可用时才用。

用 API 预置数据的方式举个例子。用 Playwright 的 request fixture 创建测试商品:

import { test } from '@playwright/test'; async function createProduct(apiContext: any) { const response = await apiContext.post('/api/products', { data: { name: 'E2E专用商品', price: 99, stock: 10, }, }); const body = await response.json(); return body.productId; } test('下单流程', async ({ request, page }) => { const productId = await createProduct(request); const userId = await createUser(request, { role: 'normal' }); // 用 userId 登录(通过设置 cookie 或自定义 token) // 走 UI 下单流程 });

测试之间严格用唯一前缀来命名数据,比如e2e-smoke-${Date.now()}-商品名,跑完用例后立刻清理遗留数据。这一套流程跑顺之后,e2e 的偶发失败率能下降一大截。

5.2 环境指纹与隔离

测试环境隔离这事,看起来是运维的活,但其实直接决定 e2e 的稳定性。我们踩过最惨的坑是:测试环境不止一个团队在用,某个老哥在环境上改了一批数据去做手工验证,结果 e2e 跑得正欢的时候突然报“商品不存在”,整个流水线红了一个下午。

后来我们把 e2e 跑在独立的一套环境上,或者至少保证数据层面完全独立——每个团队一个数据库 schema,或者一份独立数据集。如果条件实在不允许,那就退而求其次:所有 e2e 用例的数据必须带固定前缀,而且不依赖环境里已有数据的具体状态,每个用例都在执行开始时自建数据、结束时自清数据。

这里又体现出 profile 的价值了:smoke 档可以跑在主测试环境上,因为它要的是核心路径的快速验证;regression 档最好跑在独立环境上,因为它的数据敏感度更高、执行时间更长、对外部干扰的容忍度更低。

5.3 清理与回滚

测试数据创建再多,不清理就是给自己挖坑。清理策略我一般会在全局的 afterAll 里处理:

test.afterAll(async ({ request }) => { await cleanOldE2EData(request); });

清理动作不只能删自己这次跑的用例数据,还能顺带清理上次跑挂之后残留的“孤儿数据”。如果发现自己维护的数据清理逻辑越来越复杂,那基本说明整个测试数据架构该重构了——比如用固定的几个标准用户+标准商品池,让用例只读不写,减少清理负担。不过“只读不写”需要业务层配合,适合成熟稳定、核心数据变化不频繁的系统。

6. 稳定性和效率:e2e 用例写好之后的三件事

6.1 合理使用等待策略,告别无脑 sleep

很多新手写 e2e 时最喜欢干的一件事:page.waitForTimeout(3000),等三秒再说。这种写法十有八九会带来两个问题:一是浪费时间,本来一秒就能完成的操作非要等三秒;二是三秒不够的情况照样失败。

正确的姿势是用 Playwright 的自动等待机制,并且显式地等“你想等的东西”。想等一个请求完成,可以用page.waitForResponse:

await page.getByRole('button', { name: '提交订单' }).click(); const responsePromise = page.waitForResponse('**/api/orders'); const response = await responsePromise; expect(response.ok()).toBeTruthy();

想等某个元素出现在页面上,就用expect(locator).toBeVisible()。想等某个 URL 跳转完成,就用page.waitForURL()。

这些写法的共同点是:等待的目标明确且最小化。页面何时渲染完,回调里该做什么就做什么,不做多余等待。整个用例跑下来,速度快、稳定性高。

6.2 并行执行与资源分配

Plawright 默认是串行跑用例的,但我们可以通过配置让多个 worker 并行执行。fullyParallel: true开启后,不同的 spec 文件可以在不同的 worker 进程里并行跑,速度提升非常明显。

但并行是有代价的:并发数越高,对被测环境和测试机器资源的要求就越高。如果你的测试环境扛不住并发请求,跑出了各种 500、超时,那反倒不如降低并发数、增加单 worker 的稳定性。

我常用的并发达标是:本地开发时workers: 4,CI 里根据机器规格设为workers: 6到8之间。如果发现某个用例在并发下偶发失败,优先排查是不是它依赖了会被其他用例修改的资源。

6.3 稳定的用例从测试设计开始

最后说一句掏心窝的话:e2e 稳定性问题,很多不是工具问题,而是用例设计问题。你把一个长达十步的用户流程写成一个巨大的测试方法,中间任何一步出问题,诊断成本都很高。更好的做法是:把大流程拆成多个测试步骤,每个步骤只验证一个关键状态。比如:

test('完整下单流程', async ({ page }) => { // 步骤1: 登录并保存状态 await test.step('登录', async () => { await login(page); }); // 步骤2: 搜索商品 await test.step('搜索商品', async () => { await page.getByRole('textbox', { name: '搜索' }).fill('手机'); await page.keyboard.press('Enter'); }); // 步骤3: 加入购物车 await test.step('加入购物车', async () => { await page.locator('.product-card').filter({ hasText: '手机' }).first().click(); await page.getByRole('button', { name: '加入购物车' }).click(); }); // 步骤4: 结算 await test.step('提交订单', async () => { await page.getByRole('button', { name: '去结算' }).click(); await expect(page).toHaveURL(/\/checkout/); }); });

用test.step包裹每一步,失败时报告里会精确定位到哪一步挂了,排查快很多。

7. 常见问题排查实录:那些年我们踩过的坑

7.1 用例偶发失败(Flaky Test)怎么处理

这是我们团队一开始最头疼的问题。某个用例单独跑十次能过九次,全量跑的时候一变并发就挂。后来我们把处理流程固定成了一套系统性的排查法:

第一步,先看重试机制。如果retries: 2之后还是失败,说明大概率不是偶发网络抖动,而是逻辑问题。第二步,打开失败用例的 trace 和视频,从用户视角回放一遍,看操作到底卡在哪一步。第三步,看网络面板,检查失败的时候是否有接口返回了异常状态码,这一步能直接定位到后端依赖。第四步,确认测试数据是否孤立,看是不是被其他用例污染了。

最经典的一个案例:我们有个“订单列表分页”的用例,偶发失败。翻 trace 发现,失败时列表第二页展示的是另一个团队造的脏数据,原本该显示三条记录,结果那里有六条,分页按钮“下一页”不存在了。最后解决方式是给测试数据增加严格的前缀过滤规则,列表查询接口也支持按前缀过滤,问题和这个用例本身没有任何关系。

7.2 常见的几个高频坑

我整理了一份速查表,里面有我实际碰到过且反复出现的问题:

现象根因处理建议
按钮明明存在但点击无反应元素被其他浮层遮挡,或有 loading 覆盖层用expect(locator).toBeEnabled()先断言,或点击前先关闭弹层
页面跳转后断言找不到新元素前端是 SPA,URL 变了但 DOM 老元素还在用toHaveURL先确认跳转完成,再定位新元素
测试在 CI 里特别慢worker 并发数太高/太低,或浏览器启动资源受限调整 workers,观察 CPU 和内存占用
测试接口调用报 401登录态过期或 token 未在请求带上用 storageState 或重新登录,检查请求头
断言明明失败但没截图没有配置 screenshot 选项设置screenshot: 'only-on-failure'

7.3 元素定位的独家技巧

最后分享一个我个人的定位哲学:能不用 XPath 就不用 XPath,能用 role 就不用 class,逼不得已才用 CSS。为什么这么强调?因为 XPath 和 CSS 类名对前端重构特别敏感,而 role、label、文本内容这类语义化属性,跟用户真实感知高度一致,前端改了样式不影响它们。

但如果真的只能用 CSS 定位某个复杂组件,记得选最有辨识度的约束条件。比如:

const row = page.locator('tr').filter({ hasText: '订单号A12345' }); await row.getByRole('button', { name: '取消订单' }).click();

这是“先找到包含特定文本的那一行,再在那一行里找操作按钮”的经典做法,比写一长串tr:nth-child(3) > td:nth-child(2) > button这种脆弱定位强得多。

8. 关于 e2e 和 e2e profile,我最后想说的几句

做 e2e 测试这几年,我最大的体会是:e2e 不是一个“跑起来就行”的东西,它需要的是持续维护、持续调优。工具选型虽然重要,但真正决定 e2e 成败的,是你怎么设计用例、怎么管理数据、怎么拆分 profile、怎么在 CI 里合理地调度它。前三个月你会觉得“这玩意儿怎么这么难伺候”,熬过那个阶段,等核心链路的用例稳定运行之后,它给你带来的信心是其他任何测试手段都给不了的——因为每次发版前,你知道真正的用户路径已经被机器替你走过了。

最后再分享一个小技巧:给 e2e 用例的执行时间设一个“预算”。比如 smoke 档必须在 10 分钟内跑完,超过就说明用例太多或者断言太重,该优化了。跑得快的 e2e 才能被频繁执行,频繁执行的 e2e 才有资格成为质量门槛。不要让你的 e2e 变成一天跑一次还总失败的“仪式性测试”,那样的自动化,不做也罢。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询