2026-09-21

Playwright 从入门到实战:我把 UI 自动化稳定性从 60% 提到 95%

先说一个让我至今印象深刻的事。

去年下半年,我们团队的 Selenium 测试套件遇到了一个诡异的问题:同样的代码,在 Chrome 89 上跑得好好的,Chrome 90 发布后突然开始随机失败。排查了整整两天,最后发现是浏览器更新后元素点击行为的细微差异导致的。这种“浏览器一更新,测试就挂”的情况,我们每个月至少遇到三五次。

那段时间,CI 报告上永远是红红绿绿一片。团队里流传一句话:“别管测试结果,看运气。”说实话,这话听着就让人难受。

后来我决定动手改造。引入 Playwright 之后,花了大概两个月时间,把核心场景的稳定性从 60% 左右提到了 95% 以上。这个过程踩了不少坑,也积累了一些真实有用的经验,今天分享出来,希望对同样在跟 flaky test 斗争的朋友有所帮助。

一、为什么 Selenium 的稳定性问题这么难治?

Selenium 的工作原理是:你的代码 → WebDriver 协议 → 浏览器驱动 → 浏览器。多了一层协议翻译,天然增加了不确定性和延迟。

更关键的是,Selenium 本身不负责等待。你必须自己处理元素什么时候出现、什么时候可点击、什么时候加载完。于是大家的代码里充斥着这样的写法:

time.sleep(3)  # 祈祷元素已经加载完了

或者更“专业”一点的显式等待:

element = WebDriverWait(driver, 10).until(    EC.presence_of_element_located((By.ID, "dynamic-element")))element.click()  # 等等,元素是出现了,但真的可点击吗?

presence_of_element_located 只保证元素存在于 DOM 中,不保证可见、不保证可用、不保证不被遮挡。所以你还需要判断可见性、可点击性……一套下来,等待代码比业务代码还长。

而 Playwright 的思路完全不同——它直接通过 DevTools 协议与浏览器通信,没有中间商赚差价,而且把等待逻辑内建到了每一个操作里。

二、真正让稳定性上来的几个关键动作

1. 别再用 time.sleep 了,Playwright 会自己等

这是提升稳定性最立竿见影的一步。

Playwright 在执行 click()、fill() 等操作之前,会自动做一系列检查——元素存在于 DOM 中、元素可见、元素启用、元素稳定(不在动画中)、元素没有被遮挡。只有全部通过,才会真正执行操作。

也就是说,你写:

await page.getByRole('button', { name: '提交' }).click();

Playwright 内部会自动等这个按钮变得可点击,如果 30 秒内还没准备好才会报错。你完全不需要自己写任何等待逻辑。

我们迁移后的第一个版本,光是删掉那些零散的 sleep 和显式等待,测试的假失败率就降了将近一半。

但要注意一点:自动等待解决的是“元素能不能操作”的问题,不解决“数据有没有返回”的问题。比如你点击了“查询”按钮,按钮是可点击的,但后端数据还没回来,页面上还是空列表。这种情况需要显式等待:

// 等待接口返回后再断言await page.waitForResponse(resp =>  resp.url().includes('/api/orders') && resp.status() === 200);await expect(page.getByTestId('order-list')).toBeVisible();

核心原则:用明确的信号同步,而不是靠猜时间。等待网络请求完成、等待元素出现、等待 URL 变化,都比 sleep 靠谱一百倍。

2. 定位器选对了,测试就稳了一大半

我们之前 Selenium 的定位器大量使用 CSS 和 XPath,比如 .btn-primary > div:nth-child(2) > span。这种选择器只要前端调整一下 DOM 结构就废了。

Playwright 推荐优先使用面向用户的属性来定位。官方给出了明确的优先级:

第一优先级:getByRole

await page.getByRole('button', { name: '登录' }).click();

按 ARIA role 和可访问名称定位,反映的是用户和辅助技术看到的东西,不依赖 DOM 结构,前端怎么重构都不影响。

第二优先级:getByLabel / getByPlaceholder

await page.getByLabel('用户名').fill('testuser');await page.getByPlaceholder('请输入搜索关键词').fill('Playwright');

第三优先级:getByTestId

await page.getByTestId('login-submit').click();

这个需要团队协作,要求前端在核心元素上加 data-testid 属性。一开始推可能会有些阻力,但一旦形成规范,收益非常大——它是代码和测试之间最稳定的契约。

我们团队的做法是在 PR 检查中要求核心 UI 元素必须加测试 ID,配合 lint 规则 enforce。推行了两周之后,大家就意识到好处了——测试用例再也不会因为前端改了个 class 名就挂掉。

3. 登录态复用,别每个用例都重新登录

这是个容易被忽略但影响巨大的问题。之前我们的测试用例,每一条都从打开登录页开始,输入用户名密码,点击登录,等跳转……光是登录就要十几秒,而且每一步都是潜在的失败点。

Playwright 的 storageState 机制可以完美解决这个问题:

// 全局 setup,只执行一次// auth.setup.tsimport { test as setup } from'@playwright/test';setup('authenticate', async ({ page }) => {await page.goto('/login');await page.getByLabel('用户名').fill('testuser');await page.getByLabel('密码').fill('password');await page.getByRole('button', { name: '登录' }).click();await page.waitForURL('/dashboard');await page.context().storageState({ path: 'auth.json' });});

然后在 playwright.config.ts 里配置:

export default defineConfig({  projects: [    { name: 'setup', testMatch: /auth\.setup\.ts/ },    {      name: 'chromium',      use: {        ...devices['Desktop Chrome'],        storageState: 'auth.json',      },      dependencies: ['setup'],    },  ],});

这样每个测试启动时就已经处于登录状态,又快又稳。我们迁移后,单个用例的平均执行时间从 18 秒降到了 6 秒左右。

4. 测试数据用 API 准备,别走 UI

这也是一个大坑。很多测试的数据准备是通过 UI 操作的——比如先通过页面创建一个订单,然后再验证订单列表。

问题在于,UI 操作本身就是不稳定的来源。页面加载、表单验证、提交响应……每一步都可能出问题。

正确的做法是:数据准备和清理走 API,UI 只负责验证展示和交互。

test('订单列表展示', async ({ page, request }) => {// 用 API 创建测试数据const response = await request.post('/api/test/orders', {    data: { product: 'test-product', quantity: 2 }  });const order = await response.json();// UI 只验证展示await page.goto('/orders');await expect(page.getByText(order.id)).toBeVisible();});

我们后来专门建了一个 /api/test/* 的命名空间,只在 CI 环境开启,用来准备和清理测试数据。这一步做完之后,测试之间的数据污染问题基本消失了。

5. 重试要合理用,但不能当万能药

Playwright 内置了测试重试机制,在配置文件中一行就能开启:

export default defineConfig({  retries: process.env.CI ? 2 : 0,});

开启后,失败的测试会自动重试。Playwright 会把测试分为三类:passed(第一次就通过)、flaky(第一次失败但重试通过)、failed(重试也没通过)。

但是,重试不是用来掩盖问题的。如果一个测试 40% 的概率失败,那不叫 flaky,那叫有 bug。我们的原则是:重试只是给真正的偶发问题一个兜底,如果某个用例频繁进入 flaky 列表,必须去查根因。

另外,在断言层面,expect.toPass() 也很有用。它会在超时时间内反复执行断言回调,直到条件满足:

await expect(async () => {  const response = await page.request.get('/api/status');  expect(response.status()).toBe(200);}).toPass({ timeout: 15000 });

这对于验证一些“最终一致性”的场景特别有用,比如异步任务的状态更新。

6. 失败时要有足够的信息来排查

这一点可能是最容易被忽视的。以前我们的测试挂了,CI 上只显示一个红色的叉,什么信息都没有。要排查就得在本地重现,有时候根本重现不了。

Playwright 的 Trace Viewer 彻底改变了这个局面。配置一下:

export default defineConfig({  use: {    trace: 'retain-on-failure-and-retries',    screenshot: 'only-on-failure',    video: 'retain-on-failure',  },});

测试失败后,你会得到一个 trace.zip 文件,用 npx playwright show-trace trace.zip 打开,可以看到每一步操作的 DOM 快照、网络请求、控制台日志、截图……基本上等于给测试录了个“行车记录仪”。

我们最常遇到的场景是:测试在 CI 上挂了,但在本地怎么跑都过。有了 Trace 之后,直接看失败那一刻的页面状态,很多时候一眼就能看出问题——比如某个弹窗遮挡了按钮、某个接口返回了 500、页面还停留在 loading 状态等等。

Trace 只在失败时保留,不会拖慢正常通过的测试。

三、迁移后的真实数据

说了这么多方法,最终效果怎么样?

我们做了一次对比:挑 10 个核心业务流程,分别用 Selenium 和 Playwright 各跑 50 次。

指标 Selenium Playwright
平均稳定性 62% 96%
平均执行时间(单用例) 18s 6s
因定位器失效导致的失败 约 40% 约 5%
排查一个失败的平均耗时 30-60 分钟 5-10 分钟

整体套件的执行时间从 45 分钟降到了 18 分钟左右,随机失败率从 12% 降到了 2% 以下。当然,这个数据跟具体的业务场景和技术栈有关,不一定能完全复现,但方向是对的。

更有意思的是团队氛围的变化。以前发版前大家都很紧张,盯着 CI 报告刷来刷去。现在大部分时候测试报告都是绿的,偶尔红了也能快速定位问题。这种“心里有底”的感觉,比数字本身更有价值。

四、几个我踩过的坑

别在 beforeAll 里做太重的事情。 Playwright 每个测试文件是独立 worker 进程运行的,beforeAll 在每个 worker 里都会跑一次。如果里面做了耗时的数据准备,会严重拖慢整体速度。

视觉回归测试别贪多。 一开始我们想对所有页面都做截图对比,结果动态内容(时间戳、广告位、随机推荐)产生了大量无意义的 diff。后来改成只对核心页面做,并对动态区域设置 mask,才变得可用。

并发数不是越高越好。 默认情况下 Playwright 使用 CPU 核心数的一半作为 worker 数量。盲目调高会导致资源竞争,反而变慢。我们的经验是先 profile 找出瓶颈,再逐步调整。

别把所有用例都塞进每次 CI。 我们用标签把用例分成了 @smoke 和 @full 两组:冒烟测试每次提交都跑,全量回归放在夜间。这样既保证了快速反馈,又覆盖了全量场景。

写在最后

从 60% 到 95%,不是靠某一个“银弹”功能实现的,而是把自动等待、稳定定位、状态复用、API 准备数据、合理重试、Trace 调试这几件事组合在一起,一步步做出来的。

Playwright 给我的感觉是:它把很多以前需要你自己操心的事情,变成了框架的内建能力。你不需要再纠结“到底要等几秒”,不需要再写一堆 ExpectedConditions,不需要再靠日志猜失败原因。

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

  • Agent 里为什么不该什么都交给大模型?Jev 给了一个新答案 关注 霍格沃兹软件测试开发 公众号,回复「资料」...
    霍格沃兹测试开发学社阅读 197评论 0赞 0
  • AI + Playwright 自动化测试,为什么很多人卡在第一条可运行脚本? 最近很多测试、前端、全栈同学都在尝...
    霍格沃兹测试开发学社阅读 52评论 0赞 0
  • 从0到1学自动化测试该怎么规划? 导读 学自动化测试,最容易走偏的一点是:一上来就盯着工具。 有人直接学 Sele...
    霍格沃兹测试开发学社阅读 57评论 0赞 0
  • Playwright 常见面试题:给测试工程师的实战指南 最近帮几个粉丝做模拟面试,发现一个很普遍的问题: 简历上...
    霍格沃兹测试开发学社阅读 128评论 0赞 0
  • Playwright为什么老是跑不稳?12个坑踩完我终于懂了! 关注 霍格沃兹测试学院公众号,回复「资料」, 领取...
    霍格沃兹测试开发学社阅读 46评论 0赞 0

友情链接更多精彩内容