← 返回文章归档

TESTING / PLAYWRIGHT / STABILITY

做 Playwright 页面回归时,我总结出的 7 个稳定性技巧

UI 自动化最难的地方,往往不是把第一条用例跑通,而是让它在本地、CI、不同屏幕尺寸和不同网络状态下持续可信。Playwright 给了很多好工具,但真正的稳定性来自等待策略、选择器、数据、环境和调试痕迹这些细节的组合。

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 本身已经足够强,真正要补的是工程习惯:把不确定的输入收紧,把失败时的信息留够。

MORE READING

如果你也在整理自己的自动化链路,可以继续看 AI 工作流和 VPS 基础设施那两篇,它们关注的是测试之外的工程可维护性。

回到文章归档