1. 先定义“稳定”到底是什么意思
我以前会把“测试偶尔失败”归咎给工具本身,后来发现更常见的原因是标准没有定义清楚。 对我来说,一条稳定的 E2E 用例至少要满足两个条件:同一版本、同一环境重复运行时结果一致;失败时能快速定位到页面、接口、数据还是环境。
只要做不到这两点,测试带来的就不是信心,而是额外噪声。真正有价值的回归测试应该让人敢于改代码,而不是让人每次提交都先祈祷 CI 别随机红。
2. 等待策略要有层次,不要只会写 sleep
页面测试不稳定,最常见的源头就是等待时机不对。固定延时看起来简单,但它本质上是在赌机器速度和网络状态。 我现在更倾向于等待“明确的页面事实”:页面加载到某个状态、关键接口完成、目标元素可见或可点击。
await page.goto("/dashboard");
await page.waitForLoadState("domcontentloaded");
await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible();
await expect(page.getByTestId("summary-card")).toContainText("在线");
如果一个按钮要等接口返回后才可用,就不要只等按钮出现,而要等到它真正处于可点击状态。 等待条件越贴近用户行为,测试越不容易被页面内部的渲染细节干扰。
3. 选择器要优先表达语义,而不是绑定结构
如果测试大量依赖深层 DOM、复杂 class 或第几个子元素,页面稍微重构就会连锁崩。
Playwright 最值得用的地方之一,就是可以从用户视角选元素:角色、名称、可见文本,以及必要时明确维护的 data-testid。
await page.getByRole("button", { name: "保存设置" }).click();
await expect(page.getByTestId("save-success")).toContainText("已保存");
我会把测试选择器当成一种稳定接口,而不是临时抓页面结构的快捷方式。页面可以换布局,但“保存设置”这个用户动作不应该随便变。
4. 能控制的网络输入,尽量在测试里控制住
测试不稳定不一定是前端错了,也可能是后端数据波动、第三方接口超时、测试账号状态不干净。 对关键路径,我会优先把外部依赖隔离出来,用 mock 或固定测试数据验证页面逻辑。
await page.route("**/api/profile", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ name: "Junhao", role: "developer" }),
});
});
这不是逃避真实环境,而是先把问题拆开:页面逻辑是否正确、接口契约是否正确、线上环境是否稳定,应该分别验证。
5. 环境越可重复,失败原因就越容易被隔离
自动化测试最怕共享脏状态。一个用例留下的登录态、localStorage、缓存或测试数据,可能会影响后面的用例。 我更喜欢让每条关键用例有明确的前置条件,并尽可能在运行前清理状态。
- 每个测试使用独立上下文,避免 cookie 和 localStorage 串用。
- 固定时区、语言和视口尺寸,减少日期格式和响应式布局差异。
- CI 中锁定浏览器版本、依赖版本和环境变量。
- 测试数据最好可重置,可重复生成,而不是依赖某个手动账号。
当环境本身变得可控,失败就更像一个信号,而不是一团噪声。
6. 失败时别只看红字,要给自己留下可回放的痕迹
一条失败日志通常不够用。真正能节省时间的是截图、视频、trace、控制台输出和网络请求。 尤其是 CI 里偶发失败,本地复现不出来时,trace 往往比错误栈更有价值。
use: {
screenshot: "only-on-failure",
trace: "retain-on-failure",
video: "retain-on-failure",
}
我会把失败产物当成测试的一部分,而不是额外附件。它们的目的很简单:让下一次排查不用从“到底发生了什么”开始猜。
7. 最后用一份稳定性清单兜底
- 是否避免了无意义的固定延时。
- 是否优先使用语义化选择器和稳定的 test id。
- 关键接口是否有可控 mock 或固定测试数据。
- 测试是否清理了 cookie、localStorage 和共享状态。
- 本地和 CI 的浏览器、依赖、环境变量是否一致。
- 失败后是否能拿到 trace、截图、视频和网络日志。
- 移动端和桌面端是否至少覆盖了关键路径。
到最后我越来越觉得,稳定性不是某一个技巧决定的,而是很多小判断长期累积出来的结果。 Playwright 本身已经足够强,真正要补的是工程习惯:把不确定的输入收紧,把失败时的信息留够。