首页 / 资讯中心 / 文章详情

端到端测试实战:从profile1配置到Playwright稳定落地

端到端测试实战:从profile1配置到Playwright稳定落地 ★ FEATURED ARTICLE
1. 先聊清楚e2e 到底在测什么为什么每个团队最后都会补上这一环1.1 从“单测全绿一上线就翻车”说起我最早对 e2e端到端测试产生强烈执念是因为一次极其丢人的线上事故。当时项目单测覆盖率做到了 80% 多接口测试也跑得挺顺结果新版上线当天用户在下单流程里卡在了支付回调这一步。排查到最后发现前端传过去的订单编号字段是orderNo后端接口文档里写的是order_number两边单测各自都过了联调的时候靠人肉点了几轮没发现一放量就炸了。这种问题不是个例。你单测覆盖的是“函数A输入X得到Y”接口测试覆盖的是“给接口传参数A得到响应B”但真实用户从页面点进来要经历渲染、路由、状态管理、请求拦截、鉴权、数据回填、异常提示这一整条链路。任何一个环节掉链子前面所有测试都白搭。e2e 测试就是干这个的模拟真实用户在浏览器里的完整操作路径从打开页面开始到数据落库、页面反馈结束全链路一起验证。很多开发觉得 e2e 测试“没必要”“太慢”“太难维护”说实话在项目早期功能还没稳定的时候这三个吐槽我都认。但一旦系统进入多人协作、快速迭代的阶段e2e 测试就是那条安全网。没有它每次发版都是在赌运气。1.2 e2e、集成测试、单测的边界到底怎么划想把 e2e 用好首先得明白它跟另外两类测试的边界不然很容易把 e2e 写成“披着 e2e 外衣的单测”。单元测试针对最小代码单元函数、方法、组件重点验证“输入输出逻辑是否正确”。它不关心外部依赖数据库、网络请求、浏览器统统 mock 掉。集成测试验证模块与模块之间的协作。比如前端组件传 props 给另一个组件后端 Service 层调用 DAO 层拿数据这些算集成测试的范畴。e2e 测试站在用户视角通过浏览器 UI 操作走完整的业务流程。它不关心你代码内部怎么实现只关心“用户做了操作之后系统给的反馈是否符合预期”。我用一个生活化的类比单测是检验发动机单个零件集成测试是检验发动机和变速箱能不能咬合e2e 测试是整车点火从你拧钥匙开始到发动机启动、仪表盘亮灯、空调出风全流程都验证一遍。所以如果你发现自己写的 e2e 用例里大量在直接调接口、直接改数据库、直接 mock 模块那大概率方向跑偏了。e2e 的价值恰恰在于“不 mock 真实链路”让浏览器、前端代码、后端服务、数据库全部真实地跑一遍。2. 设计 e2e 方案profile 1 这套配置到底在配什么2.1 环境、账号、数据e2e 测试的三座大山真正上手做 e2e 之后你会发现写用例本身不难难的是“怎么让用例稳定地跑”。e2e 测试最让人头疼的三件事就是环境、账号和数据。环境不能在测试环境跑一套、生产环境跑一套、本地跑一套结果逻辑完全不一样。e2e 的基线环境必须固定流程要可重复。账号如果用例里用共享账号A 用例执行到一半把数据改了B 用例跑起来就挂了。账号隔离是 e2e 稳定性的第一道关卡。数据用户注册后有没有默认优惠券订单列表是空的还是有一条脏数据测试数据不做隔离用例之间会互相踩脚。这里就要引出很多人会碰到的e2e profile1这个概念了。我最初看到“profile 1”的时候也愣了一下后来才反应过来它本质上是“第一套端到端测试配置基线”。简单说profile 1 定义了这场 e2e 测试“在什么环境、用什么身份、带什么数据、跑哪些场景”的完整快照。你可以把它理解成一份测试档案张三在测试环境用一套独立账号创建了一笔测试订单这笔订单的数据不会跟别人的测试互相污染。很多成熟的 e2e 框架比如 Playwright、Cypress都支持多 profile 配置。profile 1 就是其中默认的那份 baseline它包含配置项典型值说明baseURLhttps://staging.example.com测试的目标环境地址浏览器型号Chromium通常选团队主推的浏览器做默认用户身份e2e_user_01每个 profile 绑定独立测试账号数据快照预置的商品/优惠券/订单数据保证用例启动时数据是确定的超时时间15s全局默认等待元素出现的最大时长重试策略失败重试 2 次应对偶发性的网络抖动并行分片按 spec 文件分片多机/多进程并行时的切分方式拿我们团队举例子profile 1 专门跑“主流程回归”也就是注册、登录、浏览商品、加购、下单、支付、订单查询这八条核心链路。这套 profile 每天晚上定时跑一遍跑挂了会往钉钉群推告警。2.2 为什么 profile 1 里要单独配“账号隔离”和“数据清理”可能有人觉得配置一个测试账号有那么玄乎吗还真有。我以前在另一个项目里就吃过亏测试环境只有一个统一账号test_user所有 e2e 用例共用。结果跑订单流程的用例时订单列表里同时混着十几条其他用例创建的订单断言“列表第一条是刚下单的订单”就永远不稳定——因为你不知道别的用例是不是刚好也创建了一单排序完全乱掉。后来我们给每个 profile 配了独立的账号集合profile 1 用e2e_p1_01、e2e_p1_02profile 2 用e2e_p2_01彻底把数据隔离开。与此同时每条用例执行完之后都要走“数据清理钩子”。清理方式我踩过不少坑比如直接在测试里按 Delete 键删除记录或者调 UI 删除接口——这些都不太可靠。最稳的做法是调用后端预留的测试专用清理接口或者直接连测试库执行清理 SQL。这里补充一个经验清理 SQL 别放在用例中间执行最好放在afterAll或者afterEach钩子里。因为如果你放在用例中段一旦前面的断言失败抛异常清理逻辑根本不会执行脏数据会一直留在库里下次跑用例接着中招。3. 实操从零搭建一条可维护的 e2e 测试链路3.1 选型与初始化为什么我选了 Playwright现在市面上的 e2e 主流工具就是 Selenium、Cypress、Playwright 这三家。我的个人结论是新项目直接选 Playwright老项目如果已经用 Cypress 用得很顺也没必要硬换。Selenium 是老前辈优势是生态全、语言绑定多但缺点也很明显架构太老WebDriver 协议通信效率低等待策略基本靠手写测试不稳定的时候排查起来很痛苦。Cypress 的优点是上手快、调试体验好但它的架构决定了只能在 Node.js 环境里跑而且不支持多标签页、文件的真实下载这些场景。Playwright 是微软出的走的是 CDPChrome DevTools Protocol协议跟浏览器底层打交道天生就比 WebDriver 高效还内置了自动等待、网络拦截、多浏览器支持这些非常实用的能力。我举个例子说明“自动等待”到底省了多少事。用 Selenium 的时候你要么写死Thread.sleep(3000)要么自己封装一个waitForElement()方法轮询元素是否可见。用 Playwright 的时候你不需要处理这些细节page.click()会自动等待元素出现、可见、稳定再点下去。这些时间省下来维护成本能降一个量级。初始化方式其实不复杂Node 环境装好之后一条命令就搞定了npm init playwrightlatest它会问你几个问题TypeScript 还是 JavaScript、要不要生成 GitHub Actions 配置文件、要不要装浏览器。选完就自动生成了项目骨架包括tests目录、playwright.config.ts配置文件、.gitignore这些。3.2 核心用例编写一条完整流程的断言逻辑我习惯把 e2e 用例按“用户故事”来组织而不是按页面来组织。比如“用户能用优惠券下单”是一个用例而不是“测试登录页”“测试商品页”“测试支付页”各自独立。这样才能真正模拟真实用户路径也才能暴露跨页面的集成问题。拿一条典型的“下单主流程”用例来拆解我在 profile 1 里通常会这样写import { test, expect } from playwright/test; test(profile1: 用户使用优惠券完成下单, async ({ page }) { // 1. 打开首页并登录 await page.goto(/); await page.getByRole(button, { name: 登录 }).click(); await page.getByLabel(手机号).fill(13800138000); await page.getByLabel(密码).fill(e2e_p1_password); await page.getByRole(button, { name: 登录确认 }).click(); await expect(page.getByText(欢迎回来)).toBeVisible(); // 2. 搜索并加入购物车 await page.getByPlaceholder(输入商品关键词).fill(无线鼠标); await page.getByPlaceholder(输入商品关键词).press(Enter); const firstProduct page.locator(.product-item).first(); await firstProduct.locator(.add-to-cart).click(); // 3. 进入购物车选择优惠券 await page.goto(/cart); await expect(page.locator(.cart-item)).toHaveCount(1); await page.getByRole(combobox, { name: 优惠券 }).selectOption(COUPON_50); // 4. 提交换单 await page.getByRole(button, { name: 去结算 }).click(); await page.getByRole(button, { name: 提交订单 }).click(); await expect(page).toHaveURL(/\/order\/success/); await expect(page.locator(.order-no)).toBeVisible(); // 5. 校验后端结果 const orderNo await page.locator(.order-no).textContent(); const response await page.request.get(/api/orders/${orderNo?.trim()}); expect(response.ok()).toBeTruthy(); const data await response.json(); expect(data.status).toBe(CREATED); expect(data.discountAmount).toBe(50); });这条用例看起来不长但里面藏了几个关键点第一是等待断言的时机await expect(page.getByText(欢迎回来)).toBeVisible()这种写法不是“等一秒再看”而是 Playwright 的自动重试机制它会反复检查直到超时。比page.waitForTimeout()写死强多了。第二是用角色和 label 定位元素getByRole、getByLabel这种就近原则的定位方式比page.locator(#app div div.form input)这种 CSS 路径稳太多。只要页面文案不改定位基本不会断。第三是前端断言 后端校验结合页面上显示订单成功不代表着数据库里真的创建了订单。我在最后叠加了一层 API 校验确认订单状态和优惠金额真的落库了。这一步是 e2e 区别于纯 UI 自动化测试的关键——你既要验证 UI 反馈也要验证后端结果整条链路才算闭环。3.3 并行执行与稳定性处理别让 flaky 毁了整个回归e2e 测试最典型的毛病就是 flaky不稳定时好时坏红一次绿三次。大部分 flaky 的根源是资源竞争和时序问题。资源竞争最常见的就是并行执行多个 spec 文件同时跑共享同一个测试账号、同一批测试数据。我吃过太多次亏之后总结出三条铁律每个 spec 文件用独立账号账号从环境变量或 profiles 文件里读取不要硬编码。测试数据互不干扰每个用例创建的数据都带唯一前缀比如订单号用时间戳加随机数生成。只在用例内部依赖状态不做跨用例的状态传递。这条用例创建的订单下条用例不要默认它存在。时序问题最常见的是“页面元素已经存在但数据还没渲染完”。这时候很多人会写waitForTimeout(1000)硬等我几乎不这么做因为机器性能波动太大了本地跑 1 秒够CI 机器上可能 3 秒才够。Playwright 里最优雅的解法是用expect(...).toHaveText(...)、toHaveCount(...)这类自动重试断言它会一直等到元素达到期望状态才通过。另外要说一下配置里的fullyParallel和workers。我建议小项目先不开 fullyParallel等你把用例写到三四十条单条跑一遍要半小时以上再考虑并行。并行数是双刃剑开得太多 CPU 跟不上反而更慢更不稳我们 CI 机器是 4 核 8G配workers: 3是最舒服的。下面贴一个我们 profile 1 正在用的 Playwright 配置你可以直接拿去改改import { defineConfig, devices } from playwright/test; export default defineConfig({ timeout: 30_000, fullyParallel: true, workers: 3, retries: 2, reporter: [ [list], [html, { outputFolder: playwright-report }] ], use: { baseURL: https://staging.example.com, headless: true, screenshot: only-on-failure, video: retain-on-failure, trace: retain-on-failure, viewport: { width: 1280, height: 720 }, ignoreHTTPSErrors: true, actionTimeout: 15_000, }, projects: [ { name: profile1-chromium, use: { ...devices[Desktop Chrome], testIdAttribute: data-testid, }, testMatch: /profile1\/.*\.spec\.ts/, outputDir: test-results/profile1, }, { name: profile2-firefox, use: { ...devices[Desktop Firefox], testIdAttribute: data-testid, }, testMatch: /profile2\/.*\.spec\.ts/, outputDir: test-results/profile2, }, ], });这里有几个细节值得说。testMatch做了 profile 的物理隔离profile 1 的用例全部放在tests/profile1/目录下profile 2 放在tests/profile2/目录下这样配置文件里就可以按目录区别对待。retries: 2是对 flaky 用例的兜底但这只是缓兵之计——如果一条用例经常要靠重试才能过你就得认真排查它为什么不稳定而不是无脑调大重试次数。4. 常见问题与排查技巧实录那些年我踩过的 e2e 坑4.1 “测试没过但手工点没问题”是怎么回事这大概是 e2e 测试圈子里被问得最多的一个问题。我总结下来90% 的情况是下面这几种第一种定位失效。页面结构变了比如 CSS 类名从classbtn primary改成了classbtn btn-primary。用getByRole和getByLabel能显著降低这类问题因为它们依赖的是语义化信息而不是样式类名。所以我强烈建议开发阶段就给关键交互元素加上>button>
阅读完成 · 觉得有帮助?
咨询建站