[Playwright][06]自动等待与断言:如何减少 Flaky Tests?

发布时间:2026/10/31 7:05:06
[Playwright][06]自动等待与断言:如何减少 Flaky Tests?

写 UI 自动化测试时,有一种情况很烦人:
同一条测试,运行一次成功,再运行一次却失败了。

这种结果不稳定的测试,通常被称为 Flaky Test。

明明元素就在页面上,脚本却报错说找不到;或者按钮已经点击成功了,后面的断言却失败了。

它不一定意味着业务功能存在 Bug,也可能是测试脚本没有正确处理页面加载、异步更新或执行时序。

这时候最容易想到的办法就是加一句:

awaitpage.waitForTimeout(3000);

让脚本先等 3 秒。

但问题来了:页面如果只需要 500ms,剩下的时间就白等了;如果 CI 环境比较慢,3 秒又不够,测试还是会失败。

与其猜页面需要多少秒,不如让测试去判断:我想操作的元素准备好了吗?我期待的结果出现了吗?

这也是 Playwright 的 Auto-waiting 和 Auto-retrying Assertions 想解决的问题。

一、Auto-waiting:按钮还没出现,Playwright 会怎么办?

先说实验场景

假设我们正在测试一个管理后台。

用户打开页面后,系统需要加载一些数据。为了避免用户在数据准备好之前提交操作,页面上的 Submit 按钮会先隐藏,等数据加载完成后才显示。

我们把这个过程简化一下:

  • 页面打开时,Submit 按钮是隐藏的。
  • 2 秒后,模拟数据加载完成,按钮显示出来。
  • 自动化脚本需要点击这个按钮。

这里想验证的是:按钮还没显示出来时,Playwright 会直接报错,还是会等它出现?

为了不依赖真实网站,我们用page.setContent()创建一个临时网页。

完整代码

import{test,expect}from'@playwright/test';test('Auto Waiting Demo',async({page})=>{awaitpage.setContent(`<button id="submit" style="display:none"> Submit </button> <script> setTimeout(() => { document.querySelector('#submit').style.display = 'block'; }, 2000); </script>`);awaitpage.locator('#submit').click();awaitexpect(page.locator('#submit')).toBeVisible();});

代码在做什么?

先看这一段:

<buttonid="submit"style="display:none">Submit</button>

我们创建了一个 Submit 按钮,但通过display:none把它隐藏起来。

接着:

setTimeout(()=>{document.querySelector('#submit').style.display='block';},2000);

这是网页自己的 JavaScript,意思是:2 秒后,把 Submit 按钮显示出来。

注意,这里的setTimeout()不是让 Playwright 等待,而是故意模拟一个加载较慢的页面。

最后才是 Playwright 的操作:

awaitpage.locator('#submit').click();

虽然执行到这里时按钮可能仍然隐藏,但click()不会马上放弃。

Playwright 会等待按钮满足点击条件,例如可见、稳定、已启用并且能够接收点击事件,然后再执行点击。

这就是Auto-waiting(自动等待)。

Auto Waiting Demo 的运行结果,执行click花了2.4s

这里我觉得比较重要的一点是:

Playwright 等待的不是固定的 2 秒,而是按钮什么时候真正可以点击。

如果按钮提前出现,就可以提前点击;如果按钮一直不满足条件,操作最终也会超时失败。

不过,按钮能点击,并不代表点击之后的业务操作已经完成。

这就引出了第二个实验。

二、Auto-retrying Assertions:点击成功,结果却还没出来

先说实验场景

还是管理后台,这次换成一个更常见的操作:保存数据。

页面上有一个 Save 按钮。用户点击后,系统需要向后台提交数据,保存成功才会显示:

Saved successfully

我们模拟一下这个过程:

  • Save 按钮一开始就能点击。
  • 用户点击 Save。
  • 系统花 2 秒处理保存操作。
  • 页面显示保存成功的提示。

这里要测试的不是按钮能否点击,而是:点击之后,页面有没有出现预期的成功消息?

完整代码

import{test,expect}from'@playwright/test';test('Auto Retry Assertion Demo',async({page})=>{awaitpage.setContent(`<button id="save">Save</button> <p id="message"></p> <script> document.querySelector('#save').addEventListener('click', () => { setTimeout(() => { document.querySelector('#message').textContent = 'Saved successfully'; }, 2000); }); </script>`);awaitpage.locator('#save').click();awaitexpect(page.locator('#message')).toHaveText('Saved successfully');});

代码在做什么?

先创建两个元素:

<buttonid="save">Save</button><pid="message"></p>

一个是保存按钮,另一个是用来显示保存结果的文本区域。

然后给按钮绑定点击事件:

document.querySelector('#save').addEventListener('click',()=>{setTimeout(()=>{document.querySelector('#message').textContent='Saved successfully';},2000);});

意思是:点击 Save 后,过 2 秒才把成功消息写到页面上。

这里同样没有真正调用后台 API,只是用延迟来模拟异步保存。

接下来,Playwright 点击按钮:

awaitpage.locator('#save').click();

因为 Save 按钮从一开始就可以操作,所以这一步通常很快就能完成。

但是,此时成功消息还没出现。

如果我们立即读取文字:

constmessage=awaitpage.locator('#message').textContent();expect(message).toBe('Saved successfully');

测试就可能失败,因为textContent()读取的是当时的内容,普通的expect(message).toBe()不会重新读取页面。

更合适的方式是:

awaitexpect(page.locator('#message')).toHaveText('Saved successfully');

toHaveText()属于 Playwright 的自动重试断言。

当页面文字还不符合预期时,它会在断言超时范围内继续检查,直到文字匹配,或者等待超时。

Auto Retry Assertion Demo 的运行结果

所以,click()成功只是说明点击操作完成了;toHaveText()才是在确认页面出现了预期结果。

三、这两种等待到底有什么区别?

两个实验看起来很相似,都是页面过了 2 秒才发生变化。

但 Playwright 等待的东西完全不同。

Demo 1:SubmitDemo 2:Save
初始状态按钮隐藏按钮可点击,结果为空
页面延迟发生什么按钮显示成功消息出现
主要等待方法locator.click()expect(locator).toHaveText()
等待的条件元素可以操作页面状态符合预期
对应机制Auto-waitingAuto-retrying Assertions

简单理解:

Auto-waiting 解决的是“什么时候可以操作”,Auto-retrying Assertions 解决的是“操作后有没有达到预期”。

这也是为什么实际写 E2E 测试时,通常需要把操作和验证配合起来。

例如:

awaitpage.getByRole('button',{name:'Save'}).click();awaitexpect(page.getByText('Saved successfully')).toBeVisible();

它表达的意思很明确:先点击保存,再确认保存成功。

四、回到实际项目,为什么还是会出现 Flaky Tests?

Playwright 有自动等待,是不是就不会再出现 Flaky Test 了?

当然不是。

比如,测试依赖其他用例创建的数据,但数据还没有准备好;或者页面上有多个相似按钮,Locator 匹配到了错误的元素。

这些问题并不是简单等几秒就能解决的。

还有一种容易忽略的情况:测试只验证了操作发生,却没有验证业务结果。

以之前的 Bing 搜索为例:

awaitpage.locator('#sb_form_q').fill('Playwright');awaitpage.locator('#search_icon').click();awaitexpect(page).toHaveURL(/bing\.com\/search/);

这段代码能够验证搜索后的 URL 是否符合预期,但并不能证明搜索结果内容已经加载完成。

如果测试目标是验证搜索结果,就应该进一步检查结果区域或具体内容,而不是只看 URL。

因此,我现在更倾向于先想清楚测试目标,再决定应该等待什么状态,而不是一遇到失败就增加waitForTimeout()。

五、总结

这次用两个简单的动态页面,分别模拟了按钮延迟出现和保存结果延迟返回。

我觉得最值得记住的不是某个 API,而是两个问题:

操作之前:元素真的可以操作了吗?

操作之后:页面真的达到预期状态了吗?

Playwright 的 Auto-waiting 和自动重试断言,正好分别帮助我们处理这两个问题。

相比到处添加固定等待,明确地等待元素状态和业务结果,通常更可靠,也更方便排查失败原因。

当然,Flaky Test 还可能与测试数据、执行顺序、网络和环境有关。自动等待只能解决其中一部分问题。