E2E端到端测试实战:用Playwright搭建稳定profile1冒烟用例
发布时间:2026/10/26 15:16:57 锦皓数字建站

上个月一个后台项目发版我照例先跑了 E2E 回归。命令行里看到 profile1 这套用例全部通过才放心把构建推到生产环境。我做端到端自动化测试已经五年这类项目从零搭过、从乱麻里救过今天把最有借鉴价值的部分整理出来希望能给正在搞 E2E 或者准备引入 E2E 的团队一点参考。E2E 全称 end-to-end也就是端到端测试。简单说就是让测试脚本像真人一样从界面入口开始操作输入账号、点按钮、填表单一路走到后端接口、数据库甚至第三方服务最后再回到界面验证结果。它解决的问题是“系统对外承诺的用户旅程到底还能不能走通”适合测试开发、前端工程师以及所有想把手工回归成本压下来的研发团队。很多团队会把 E2E 用例按重要程度分成几个 profileprofile1 是我们内部的第一档冒烟集合专门守用户最常走的几条主链路。这篇文章与其说在讲 E2E 原理不如说是在分享我围绕 profile1 搭建、维护一套稳定端到端测试的真实经验。1. 端到端测试的定位与设计思路1.1 E2E到底在测什么很多刚接触 E2E 的人会把它简单理解成“自动化点按钮”这是一个危险的想法。如果只为了点按钮那单元测试和组件测试早就能做了何必用一个跑起来慢、维护成本又高的东西。E2E 真正要测的是各个层级组合在一起之后的行为。我习惯用开车来类比单元测试是检查发动机某个零件的材质集成测试是把发动机、变速箱装到台架上测试它们能配合工作而 E2E 是整车路测你得真的握方向盘、踩油门、过弯道才能知道这辆车开起来是不是顺手会不会在某个路口熄火。对应到系统里就是浏览器里的点击操作要真实触发网络请求后端要真的读写数据库如果接入了第三方服务也要验证回调回来之后整个链路是否闭环。所以我们做 E2E 时不能只盯着界面上有没有元素更应该关注数据有没有在请求之间正确流转、权限在每一步是否会拦截、异常分支是不是真的出现了友好提示。E2E 不擅长发现深埋在后端角落里的小 bug它能告诉你的往往是“用户根本走不到那一步”。这也是为什么它要放在发版前而不是替代单元测试。1.2 为什么要有profile机制如果所有 E2E 用例不分角色、不分级别全部塞到一个集合里跑很快就会出现两个问题时间越来越长失败越来越难解释。比如你有几百条用例跑了 40 分钟中间有一半是因为测试数据互相污染挂掉的这时候就算标红了团队也未必能第一时间判断是线上问题还是脚本问题。所以要把测试集合按风险和价值切片。我见过团队把 E2E 分成 smoke、sanity、regression本质上也是一种 profile 机制。我们内部叫 profile1、profile2、profile3作用类似。profile1 是最高优先级的冒烟集合覆盖登录、商品列表、下单、支付回调这类核心主链路数量控制在 10 到 15 条左右要求 5 分钟内跑完并且必须稳定profile2 负责常规业务回归可以定时跑profile3 专门跑跨浏览器兼容比如 WebKit、Firefox 这些非主力浏览器。配置档位目标用例数量执行频率运行时间示例profile1核心主链路冒烟作为合入门禁10-15 条每次 PR、每次发版前3-5 分钟profile2常规业务回归80-200 条每夜或每个迭代20-40 分钟profile3跨浏览器/设备兼容核心用例的子集每周或大版本前30-60 分钟从成本角度看profile1 是最值得投资的。它就像一个门店的试营业先把最赚钱、最容易被顾客投诉的流程跑一遍有问题立刻停业调整而不是等正式开业那天让客人满世界找入口。1.3 哪些场景适合E2E哪些不适合这些年我踩过最大的坑就是试图让 E2E 干所有事。同事曾把日期选择器的每个月份切换都写成 E2E结果每次日历组件一升级测试就要跟着改一遍最后大家都不敢动日历组件的代码了。这明显是过度使用。适合放进 E2E 的场景通常有这几个特征用户频繁使用、跨系统跨服务、出现问题影响大。注册登录账号体系挂了后面所有流程都是空话。核心购物链路搜索、详情、加购、结算、支付回调。权限隔离不同角色登录后看到的菜单和数据不同。第三方集成登录鉴权、短信回调、支付通知。多步骤表单申请、审批、流转状态每一步都有数据变化。不太适合放 E2E 的场景也有不少复杂算法和纯逻辑计算单元测试更合适。海量数据校验用数据校验任务而不是让浏览器一页页翻。像素级视觉比对截图对比工具更专业。一次性活动页面存活两周的活动用例刚写好就要删纯浪费。我以前总结过一个判断标准如果一条 E2E 用例每周要修超过一次那说明它要么不应该被自动化要么设计有问题。正常的代码改动不应该让 E2E 频繁失效除非你把不该放进去的场景放错了层。2. 工具选型与基础配置2.1 主流框架对比选 E2E 框架时很多人会陷入“谁名气大选谁”的误区。其实主流框架各有特点核心要看你的项目形态和团队熟悉程度。现在用得最多的三套是 Selenium、Cypress 和 Playwright。能力PlaywrightCypressSelenium跨浏览器支持Chromium、Firefox、WebKit主要是 Chromium 系Firefox 支持有限制全浏览器但需要依赖驱动自动等待内置无需手动 sleep内置较友好需要自己封装显式等待多标签页/多窗口支持较好有限官方推荐 iframe支持但是代码繁琐Trace 录制原生支持失败可回放有截图和视频一般借助第三方语言支持JS/TS、Java、Python、C#JS/TS多语言易用性学习曲线适中上手快API 友好偏重模板代码多我最终选了 Playwright TypeScript主要原因是它把自动等待做得比较靠谱不需要在代码里到处写sleep或者自定义waitForElement。另外 Trace 功能对排查 flaky 问题帮助极大后面我会专门讲。Cypress 对前端测试非常友好但如果团队需要同时跑 WebKit 这样的浏览器或者经常处理多标签场景Playwright 会更省心。选择工具还有一个隐性成本社区讨论量。遇到奇怪问题时搜索引擎能搜到的答案数量很重要。Playwright 这几年社区活跃度很高绝大多数问题都有现成解决方案这一点在维护期会体现得特别明显。2.2 理解并定义profile1配置很多团队第一次接触“e2e profile1”时以为它只是一个环境变量或者一份静态配置。我的理解不太一样profile1 是一套配置快照包含环境地址、测试账号组、功能开关、浏览器组合和用例过滤规则。单独跑 profile1 应该是很容易的事一条命令就能复现整套冒烟测试。在 Playwright 里面我们可以通过 project 和 grep 来实现这个效果。先看一个最小的playwright.config.tsimport { defineConfig, devices } from playwright/test; export default defineConfig({ testDir: ./tests/e2e, timeout: 30_000, expect: { timeout: 5_000 }, fullyParallel: false, retries: process.env.CI ? 2 : 0, reporter: [[html, { outputFolder: playwright-report }]], use: { baseURL: process.env.E2E_BASE_URL ?? https://staging.example.com, trace: retain-on-failure, screenshot: only-on-failure, video: retain-on-failure, viewport: { width: 1280, height: 720 }, }, projects: [ { name: profile1-chromium, grep: /profile1/, use: { browserName: chromium, ...devices[Desktop Chrome] }, }, ], });这里的核心是grep: /profile1/。它会让这个 project 只运行名字或标签里带profile1的用例。这样 profile1 不需要靠目录隔离而是靠标签选择。好处很明显一条用例可以同时属于 profile1 和 profile2只需要加上profile2就行用的时候用不同 project 调用。另一个关键点是baseURL用环境变量控制。profile1 可以对着 staging 跑也可以对着本地启动的测试环境跑。只要环境变量变了用例不用动。我会把密文类信息密码、Token放在 CI 的 secrets 里不让它们出现在仓库代码里。2.3 关键参数怎么调避免超时误判E2E 跑挂一半以上的原因跟超时和重试有关。我见过不少人一遇到用例超时就把 timeout 从 30 秒改成 60 秒结果问题不但没解决反而把页面白屏太久这种性能问题掩盖了。用户可不会在提 bug 前等你 60 秒。我的经验是给超时设一个“不舒适阈值”单步操作 30 秒已经很长断言在 5 秒内应该能等到结果。如果一个操作经常超过 30 秒优先考虑是不是页面慢、接口慢而不是把超时调大。调参应该是最后手段。参数建议值说明timeout30_000单条用例内每个操作的最大等待时间expect.timeout5_000断言等待元素出现的最大时间retriesCI 2本地 0重试会消耗时间本地重试没意义workersprofile1 建议 1-2并发过高容易踩测试数据冲突fullyParallelfalseprofile1 保持串行为主避免资源竞争workers是很多人忽略的参数。profile1 用例不多没必要开 8 个 worker 去抢资源。我曾经开过 4 个 worker 跑 profile1结果每个用例都在创建订单数据库里出现一堆脏数据然后互相影响。后来老老实实把 workers 调成 1稳定得一塌糊涂。并发留给 profile2 去玩冒烟测试求的是确定性。3. 实操过程从零搭建profile1用例3.1 项目初始化与目录结构如果你也是从零开始最简单的方式是执行npm init playwrightlatest它会帮你创建playwright.config.ts并安装浏览器。如果你不想用脚手架也可以手动装npm init -y npm i -D playwright/test npx playwright install chromium我习惯把目录组织成下面这样project/ playwright.config.ts package.json tests/ e2e/ profile1/ auth.spec.ts order.spec.ts utils/ auth.ts test-data.ts fixtures/ extended.ts这里有个容易忽略的点profile1 目录只是一个组织方式真正决定它是否属于 profile1 的是标签profile1。目录可以帮助人维护标签可以帮助机器选择。两者不要混为一谈。公共逻辑一定要抽出来。比如登录操作我会放到utils/auth.ts里而不是在每个用例开头重复一段获取用户名密码、填表单的代码。重复代码一旦遇到登录页改版所有用例都要跟着改一遍那时候你就知道抽公共逻辑有多重要了。3.2 编写核心用例登录、导航、核心动作、断言我们拿一个“正常下单全流程”来举例。这是 profile1 里最典型的场景也是大多数电商后台必须保证的链路。import { test, expect } from playwright/test; import { login } from ../utils/auth; test.beforeEach(async ({ page }) { await login(page, process.env.E2E_USER!, process.env.E2E_PASS!); }); test(profile1: 正常下单全流程, async ({ page }) { await page.goto(/products); await expect(page.getByRole(heading, { name: 商品列表 })).toBeVisible(); await page.getByRole(link, { name: 无线机械键盘 }).click(); await page.getByRole(button, { name: 加入购物车 }).click(); await page.getByRole(button, { name: 去结算 }).click(); await page.getByPlaceholder(收货人姓名).fill(张三); await page.getByPlaceholder(手机号).fill(13800138000); await page.getByRole(button, { name: 提交订单 }).click(); await expect(page.getByTestId(order-success)).toHaveText(订单提交成功); });这段代码每一步都有讲究。第一步page.goto(/products)没有加 baseURL 前缀因为配置文件里已经设好了。登录用的是beforeEach保证每条用例的起点都是已登录状态。这里没在beforeEach里断言登录是否成功是因为接下来商品列表的toBeVisible()已经隐式检查了页面是否正常渲染如果登录失败这一步必然超时这里相当于把两次等待合并了一次缩短了单条用例的时间。进入详情页后我用的是getByRole(link, { name: 无线机械键盘 })而不是page.locator(text无线机械键盘)因为前者可以精确定位到可点击的链接不会因为页面其他地方出现同一文案而报错。填表时用 placeholder 定位已经是比较稳定的方式前提是前端别乱改 placeholder。最后断言用的page.getByTestId(order-success)是测试专属标记。我一般在项目里和前端约定好给关键成功态、错误态节点加>const prefix e2e_${Date.now()}_${Math.floor(Math.random() * 1000)};第二尽量通过 API 去创建前置数据而不是通过 UI 一步一步点进去。比如我要测订单列表的分页先调用后端接口为当前测试账号创建 20 条订单再打开页面验证分页。如果依赖 UI 去创建 20 条订单用例不仅慢还容易在这中间挂掉。第三设定清理策略。afterAll里把本测试创建的数据清掉但如果用例失败我不会强行清理。因为失败现场的数据往往就是排查原因的关键线索保留原样比强行删除更有价值。等到 trace 看完、问题定位完再通过数据库任务清理即可。不要指望一个测试账号从 morning 跑到 night 都是干干净净的也不要用“每次测试前清库”这种粗暴方案那会导致多个并行任务互相影响。数据隔离是设计出来的不是抱怨出来的。3.4 在CI中接入profile1代码写出来不等于能持续发挥作用把它接入 CI 才是真正解放手工的开始。我们在 GitHub Actions 里用一个简单 workflow 跑 profile1# .github/workflows/e2e.yml name: E2E profile1 on: pull_request: paths: [src/**, tests/**, playwright.config.ts] jobs: profile1: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - uses: actions/setup-nodev4 with: node-version: 20 - run: npm ci - run: npx playwright install --with-deps chromium - run: npx playwright test --projectprofile1-chromium env: E2E_BASE_URL: ${{ secrets.STAGING_URL }} E2E_USER: ${{ secrets.E2E_USER }} E2E_PASS: ${{ secrets.E2E_PASS }} - uses: actions/upload-artifactv4 if: failure() with: name: playwright-report path: playwright-report/几个细节比较关键。paths过滤让 workflow 只在相关代码变更时触发减少无谓运行。如果不加过滤每次改一行文档也要等 5 分钟 E2E团队迟早会对自动化产生反感。npx playwright install --with-deps chromium一定要加--with-deps否则在新环境上可能出现缺系统库导致浏览器启动不了。运行时把环境变量从 secrets 注入账号密码不用写进代码。失败后上传playwright-report是必须的。否则遇到失败本地能跑通、CI 跑挂你都不知道 CI 上的证据在哪。现在 Playwright 的 HTML 报告里能看到完整 trace 和 console 报错排查效率提升不是一点半点。4. 常见问题与排查技巧实录4.1 测试不稳定flaky的典型原因和对策很多人刚开始跑 E2E最头疼的不是功能写不出来而是“这次过了下次挂了再跑又过了”。这种随机性会让团队对自动化失去信心。我统计过自己项目的 flaky 原因典型的有下面几类。原因表现对策等待方式不对手动 sleep 导致有时快有时慢换成内置自动等待或显式等待元素状态动画/过渡影响元素存在但仍被遮挡等待动画结束采用toBeEnabled断言网络请求不稳定接口偶尔超过 500ms提高接口响应稳定性而不是盲目加 timeout测试数据串扰多个用例操作同一账号独立账号 API 造数据定位器不规范CSS 层级一变化就找不到使用 role/testid 定位排查 flaky 的第一步永远是打开 trace。Playwright 配置里我开了trace: retain-on-failure每次失败都会录制完整的浏览器执行过程和页面快照。从 trace 里你能看到每一步操作前页面长什么样、接口返回了什么、元素是在哪一步开始失踪的。如果没有 trace盲目重跑十次再瞎猜基本就是在油锅边打转。我自己有个额外经验发现 flaky 后不要急着改代码先在本地连续跑五次看失败率。如果五次都过再回到 CI 跑几次。有时候问题只出现在 CI 环境比如并发资源不够、网络慢这些在本地根本复现不出来。4.2 定位器写不对是最大的时间黑洞E2E 出错一半以上是定位器问题。我见过最糟糕的一种写法是这样await page.locator(#app div div.card-content button).click();这串选择器今天能用明天前端同事在中间加了一个div测试就立刻挂了。其实用户根本不会关心按钮外面包了多少层 div他们只关心这个按钮说“提交订单”并且能点。我的定位器优先级是首选getByRole(button, { name: 提交订单 })语义清晰。其次getByTestId(order-success)需要和前端约定。再其次getByPlaceholder适合表单输入框。兜底CSS 类名但要避免依赖多层父子结构。不要图省事用page.locator(text提交订单)。如果页面上有两个按钮都叫“提交订单”比如一个在弹窗里、一个在底部栏这个定位器会直接报 strict mode violation错误信息倒是很清晰但你已经浪费了一次运行时间。我后来把这条要求写进了团队测试规范不允许在 E2E 用例中出现类似div div span button的层级链。这看起来像洁癖其实是为了减少维护成本。前端改结构是非常正常的事如果每一次结构改动都要连带改测试那自动化就变成了项目的额外负担。4.3 测试数据隔离与并发冲突profile1 推荐 low 并发但你还是可能遇到多个 worker 并行跑同一套用例的情况。最典型的冲突是两个 worker 同时用同一个登录账号第二个 worker 登录成功之后第一个 worker 的会话被踢下线然后它下一步操作全部因为未登录而失败。解决办法有几个层次如果账号资源充足给每个 worker 配独立账号比如用户名带workerIndex。如果账号有限老老实实把 workers 设为 1串行执行。共享数据不要用 UI 创建通过 API 初始化。workerIndex的使用可以这样const username user_${test.info().workerIndex};但这个方案依赖账号体系允许动态创建用户。如果测试环境不能动态建号那还是回到限制并发最稳妥。说到底E2E 的价值在于稳定而不是跑得多快。profile1 只有 10 条用例串行也就多两分钟换来的确定性非常值。还有一类问题是公共数据被改。比如有一个商品A 用例把它下架了B 用例还要用它来加购结果 B 必然失败。这种情况需要让 A 用例在完成后恢复现场或者在数据设计上不让用例互相依赖。我喜欢把这类用例明确标注成“会修改共享数据”放到 profile2 单独串行执行而不是掺进 profile1。4.4 profile1的维护纪律如何避免“经常红”最后想聊聊和代码关系不太大、但决定能不能长期坚持下去的事情维护纪律。很多团队的 E2E 项目头一个月做得风生水起三个月后所有用例全红然后大家就假装它不存在了。原因基本都是“只进不出只写不养”。profile1 尤其要做减法。我要求团队遵守几条简单的规矩新增一条 profile1 用例必须先能稳定连续跑 10 次如果一周内这条用例红过三次就要复盘是产品逻辑变了、数据不稳定还是它根本不该放 profile1。不要让 profile1 里的用例数量无限膨胀它只服务最高优先级的主链路。遇到失败时不要急着点重跑。重跑确实能让你暂时绿掉但核心问题没解决下次还会来。正确的流程是打开 trace、看失败步骤、判断是环境问题还是用例问题、修复或调整再重跑。我个人在实际操作中的体会是profile1 最怕的不是写不出来而是写得太快。把一条真正核心的用户旅程打磨到连续跑 20 次都不失败再考虑加第二条。另外一个小技巧每次改完公共组件先手动跑一下 profile1 再提交代码五分钟出结果比在代码评审里争论半天有效得多。现在这套 profile1 已经成了我们团队合入代码的门禁坚持几个月后发版前的手工回归时间从两小时压缩到了十分钟这才是端到端测试真正应有的价值。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。