接手过不少项目的自动化测试,发现一个挺普遍的现象:一说做 E2E 测试,团队第一反应就是找个框架把页面点一遍,跑起来就算完事。等真上了 CI(持续集成),问题就来了——用例偶发失败、数据互相污染、定位器一改全崩,最后整个测试套件被当作摆设,没人敢看执行结果。这轮 E2E 重构,就是想把这堆烂摊子彻底理清。
我想聊的这套方案,核心是围绕一个词:profile1。我把测试方案按业务场景拆成了不同的 profile(配置档案),而 profile1 代表的是第一套、也是最重要的一套回归配置——覆盖用户主流程从登录、查询、操作到结果校验的完整链路。它不是某个测试文件的名称,而是一整套环境、数据、用例、报告的组织方式。这套思路跑通之后,后面再扩展新场景,就是复制一套 profile 的事。
这篇文章适合正打算做 E2E 测试、或者已经做了一半但被稳定性问题折磨的人。我会把设计思路、实操步骤、踩坑记录都拆开讲,尽量少说废话,直接给能落地的方案。
1. 整体设计与思路拆解:E2E 测试为什么需要一套 profile 体系
1.1 从“能跑”到“能信”:E2E 测试的三个层次
很多团队对 E2E 测试的认知停留在“模拟用户点一点”。这没毛病,但点完之后能不能给出可信的结论,就是另一回事了。我把 E2E 测试的成熟度分成三个层次:
- 第一层:能跑。框架搭起来了,几条用例能够在本机手动执行通过,截图能看到页面操作过程。
- 第二层:可重复。同一套用例在干净的测试数据下,连续执行 3 次以上结果稳定,不依赖人工干预。
- 第三层:能信。每次提交代码都能自动触发,失败用例能清晰指出是功能回归、环境问题还是数据问题;测试报告可以作为发布决策的参考依据。
我这轮重构的目标,就是把自己的测试方案推到第三层。而要实现“能信”,单靠写用例根本不够,必须从测试环境、数据准备、用例组织、执行调度、结果反馈这几个维度同时下手。把它们捆成一个整体,就是 profile。
1.2 为什么把方案拆成多个 profile,而不是一个大而全的测试套件
如果你试过把所有用例塞进一个测试目录,执行时间超过半小时,你就知道有多难受。我的思路是:按测试目标和执行频率拆分成多个 profile。
| Profile | 定位 | 执行频率 | 覆盖范围 |
|---|---|---|---|
| profile1 | 核心主流程回归 | 每次提交代码/每日定时 | 登录、首页、关键业务操作、结果校验 |
| profile2 | 深度业务场景 | 每日定时/每周 | 跨模块长链路、异常流程、权限边界 |
| profile3 | 系统级稳定性 | 夜间/每周 | 长时间运行、内存占用、并发冲突 |
这样拆分有两个好处。一是反馈速度。profile1 尽量控制在 5 到 10 分钟内跑完,开发提交代码后能快速拿到结果;复杂场景放到 profile2、profile3,慢慢跑也不影响日常迭代节奏。二是排障效率。每个 profile 对应一套独立的测试基线和数据准备逻辑,当某条用例失败时,能很快定位是“主流程被破坏”还是“深度场景的边界条件没满足”,不用在一个大杂烩套件里翻江倒海。
这篇文章主要围绕 profile1 展开,因为它是最基础、也最容易复制的模板。
1.3 工具选型:为什么我选 Playwright 而不是 Selenium 或 Cypress
工具选型这件事,只要不是被公司强绑定的,我建议优先考虑测试代码的可阅读性和调试体验。在这轮方案里,我选的是 Playwright,理由如下:
- 自动等待是内置的。Selenium 时代写
WebDriverWait是家常便饭,写错等待条件就会出现“时好时坏”的梦魇。Playwright 的定位器会自动等待元素可见、可点、稳定,代码里几乎不用写sleep。 - 多浏览器支持开箱即用。Chromium、Firefox、WebKit 一套 API 全跑,不用自己折腾 driver。
- 调试体验是真的好。
--trace参数能录下完整的执行轨迹,操作步骤、网络请求、控制台日志、页面截图全都有。排查问题比对着日志猜状态不知道高效多少。
Cypress 我也用过,它的交互式调试器和时间回溯很惊艳,但它的执行模型和 Playwright 有本质差异:Cypress 的测试代码运行在浏览器里,而 Playwright 的测试代码运行在 Node 进程里,通过协议驱动浏览器。这意味着 Playwright 在跨域、多标签页、iframe 处理上更灵活。对于要做复杂业务链路验证的场景,Playwright 更顺手。
2. 核心细节解析与实操要点:profile1 要管住哪些东西
2.1 profile1 的配置档案里到底放什么
我把 profile1 设计成一个配置文件加上一套约定的目录结构。配置文件的定位是“描述这组测试的运行环境、数据基线、报告输出等一切外部依赖”。
你可以把它理解成一套“出车检查单”——出发之前把所有该确认的都确认一遍。在我的项目里,profile1.config.ts的核心内容包括:
- baseURL:被测系统的测试环境地址,统一收敛到一个入口。
- testEnv:环境标识,比如
staging、qa,用来区分同一套用例在不同环境的参数差异。 - credential:测试账号,我一般按角色区分(管理员、普通用户、只读用户),不同场景用不同身份。
- dataSnapshot:数据基线,描述执行前需要构造多少测试数据、覆盖哪些业务分支。
- timeout:全局超时配置,不同操作类型给不同阈值,避免单个慢操作拖死整个任务。
- reporter:结果报告输出方式,我一般配成 HTML + JSON,方便人看和程序解析并存。
2.2 测试数据准备:E2E 测试最容易被忽视的环节
在 E2E 测试里,数据层的失败往往比业务逻辑失败更让人抓狂。我在 profile1 里对数据处理做了三条规定,执行下来的效果很明显:
- 独立数据池。测试环境的数据库必须和开发环境隔离,同时不让测试用例直接连数据库操作。造数统一走 API 接口,或者通过后台管理界面操作。这样既贴近真实用户行为,也避免脏数据直接落到库里。
- 数据留痕。每条用例执行前记录自己依赖的数据快照,执行后再清理或者打标记。如果做不到彻底的清理,起码保证同一条用例重复执行时不会读到上一次的残留。
- 避免硬编码主键。用例里需要用到的数据 ID 一律在 setUp 阶段动态获取,不写死。我曾经见过某条用例把订单号硬编码,结果三个月后那条订单被人工处理了,用例就开始随机失败。
这些做法的核心逻辑是:让测试数据成为测试代码的可声明依赖,而不是碰运气的潜在变量。
2.3 元素定位:少用文本匹配,多给接口留钩子
UI 自动化里最脆的部分就是元素定位。换个文案、改个样式,测试就挂了。在 profile1 的开发规范里,我强烈建议前端在关键交互元素上加上稳定的>// 别指望这段代码能稳定太久 await page.click('button:has-text("确 定")');
稳定的代码长这样:
//>mkdir e2e-profile && cd e2e-profile npm init -y npm install -D @playwright/test npx playwright installnpx playwright install会下载浏览器内核,这一步在国内可能需要等一会儿,可以选用系统已有的 Chromium 来跳过下载,不过直接用官方下载的版本兼容性最省心。
接下来是项目目录结构,我习惯这样组织:
e2e-profile/ ├── src/ │ ├── configs/ │ │ └── profile1.config.ts # profile1 的配置档案 │ ├── fixtures/ │ │ └── index.ts # 测试夹具,负责登录态和数据准备 │ ├── helpers/ │ │ ├── api.ts # 造数接口调用封装 │ │ └── db.ts # 数据清理/校验工具(仅测试环境) │ ├── specs/ │ │ └── profile1/ │ │ ├── login.spec.ts # 登录场景 │ │ └── core-flow.spec.ts # 核心业务流场景 │ └── utils/ │ ├── reporter.ts # 报告处理器 │ └── env.ts # 环境变量解析 ├── playwright.config.ts # Playwright 全局配置,按 profile 分类执行 └── package.json目录结构的原则是按 profile 分目录,而不是按功能模块分目录。后续加 profile2、profile3 只需新增目录,不会影响已稳定的 profile1 的执行。
3.2 配置 profile1:先给测试套件定基线
核心的配置写在playwright.config.ts里,我用 projects 机制来区分不同 profile:
// playwright.config.ts import { defineConfig } from '@playwright/test'; import { profile1Config } from './src/configs/profile1.config'; export default defineConfig({ testDir: './src/specs', timeout: 60_000, // 单条用例超时:60秒 expect: { timeout: 10_000 }, // 断言等待时间:10秒 retries: 1, // 失败重跑一次,先区分偶发问题 workers: 4, // 并行 worker 数,按 CI 配置调整 use: { baseURL: profile1Config.baseURL, trace: 'retain-on-failure', // 失败保留 trace,便于揪根因 screenshot: 'only-on-failure', }, projects: [ { name: 'profile1', testMatch: /profile1\/.*\.spec\.ts/, use: { ...profile1Config.use, }, }, // 后续 profile2、profile3 在这里扩展 ], });testMatch是关键写法。它把 profile1 目录下的用例隔离开,跑的时候用--project=profile1就只会执行该 profile 的用例。这样本地调试、CI 定时任务都能精准指路,不用全量跑。
3.3 写一个标准的 profile1 夹具
夹具(fixture)是 Playwright 的高频工具,它负责把“外部依赖准备好了再进用例”这件事变成标准化动作。我的 profile1 夹具负责三件事:登录、数据准备、环境校验。
// src/fixtures/index.ts import { test as base } from '@playwright/test'; import { login } from '../helpers/auth'; import { ensureBaseData } from '../helpers/api'; export const test = base.extend({ // 每个用例执行前,自动完成登录 authenticatedPage: async ({ page }, use) => { const user = { name: 'profile1User', role: 'admin' }; await login(page, user); await use(page); }, // 数据基线:确保核心业务所需的批次数据已存在 baseData: async ({}, use) => { await ensureBaseData('profile1'); await use({ batchId: 'BATCH-PROFILE1' }); await clearTestRemains('profile1'); }, });这里有个容易忽略的细节:推荐的登录方式不是每次用例都走一遍 UI 表单,而是先走 API 拿到会话凭证,再注入到浏览器上下文。本质没变,但速度快得多。UI 登录只保留一条用例做专门验证,其他用例不应该重复造轮子。这个取舍是 profile1 能稳定跑进 10 分钟的关键之一。
3.4 写一套完整的主流程测试用例
我拆了一条典型的“登录 → 查询列表 → 提交工单 → 校验结果”链路,可以对照参考。
// src/specs/profile1/core-flow.spec.ts import { test, expect } from '../../fixtures'; test.describe('profile1 核心主流程', () => { test('用户能完成工单提交全流程', async ({ authenticatedPage }) => { const page = authenticatedPage; // 1. 进入工单列表页 await page.goto('/tickets'); await expect(page).toHaveURL(/\/tickets/); // 2. 查询已有工单,确认列表渲染 await page.getByTestId('search-input').fill('P1'); await page.getByTestId('search-button').click(); await expect(page.getByTestId('ticket-row')).first().toBeVisible(); // 3. 创建新工单 await page.getByTestId('create-ticket-button').click(); await page.getByTestId('ticket-title').fill('自动化提交-Profile1'); await page.getByTestId('ticket-priority').selectOption('high'); await page.getByTestId('submit-ticket').click(); // 4. 结果校验 await expect(page.getByText('提交成功')).toBeVisible(); await expect(page).toHaveURL(/\/tickets\/\d+/); }); });这段代码有几点值得注意。一是步骤都带明确的业务含义,注释不是废话;二是每一步都有断言,不是最后才验证;三是用前端埋的>npx playwright test --project=profile1 想看更详细的执行过程,可以加上 在 CI 里,我的一般做法是:提交代码触发 在 E2E 测试的世界里,“偶发失败”是最让人头疼的词语。但大多数偶发失败都不是灵异事件。我总结了六个排查方向,每个都真实踩过: 每次遇到偶发失败,先把 trace 和截图下载下来,对比重试前后的差异,大多数问题都能在 10 分钟内定位。 这个约定成本极低,但价值极高。尤其是在多个环境同时存在、切换频繁的时候,它帮我挡住了至少三次“连着生产环境跑测试”的灾难。 跑通了两轮迭代后,我给这套方案做了一次复盘。总结下来,最关键的几个决定是:把方案拆成 profile、把测试数据当作一等公民、用 Playwright 的自动等待替代手工等待、把夹具标准化。这四个决定共同解决了一个核心痛点——让 E2E 测试从“偶尔跑一次的实验”变成“可靠且有纪律的工程环节”。 其中最难的不是技术实现,而是团队共识。如果团队里没人愿意承认测试脚本需要投入维护,任何技术方案都会腐烂。反过来,只要大家认可 E2E 测试的价值,哪怕初始写得不完美,也能通过迭代逐步逼近稳定。 从 profile1 开始另一点好处是门槛低。它只覆盖核心主流程,用例数量控制在 20 条左右,单次执行控制在 10 分钟以内。这套体量很好维护,出问题也能快速解决。等团队有了信心,再逐步扩展 profile2、profile3,整个过程会平滑很多。 我个人在实际排查中的体会是:E2E 测试的稳定性其实是一个“准入门槛”,跨过它,自动化测试才真正从成本变成资产。希望这套 profile 的思路能给正在折腾 E2E 的人一些可复用的启发,少踩几个我踩过的坑。--trace on来收集所有步骤的轨迹文件。报告用默认的 HTML 即可,打开后能看到每个用例的执行视频、网络请求时间线、控制台输出。profile1,每天凌晨跑profile2和profile3;报告上传到固定的存储路径,并解析 JSON 报告把失败信息发送到团队群。关键是失败即告警、结果可追溯,这两点做到了,测试才算真正融入了研发流程。4. 常见问题与排查技巧实录:那些年我踩过的坑
4.1 测试偶发失败?先别急着改代码,查这六个地方
排查方向 典型表现 排查方法 网络波动 同一用例重试后通过 看 Playwright trace 里请求耗时是否异常 数据残留 查询结果时有时无 检查用例执行前是否清理了上一轮数据 元素遮挡 按钮明明存在但点不到 审查 trace 截图,看是否有遮罩层、弹窗 接口延迟 页面内容渲染不稳定 把固定等待改为元素自动等待 环境配置 某台 CI 机器执行特别慢 用 --workers=1对比串行执行是否稳定前端口径变化 文案或样式微调导致失败 检查是否有测试专用标识可替代文本匹配 4.2 定位器失效:老项目没有>// 所有用例开头的保护性断言 await expect(page.locator('html')).toHaveAttribute('data-env', 'staging');
5. 对 profile 体系做一次复盘:哪些决定是关键性的