目录
一、sleep堆得越多,脚本崩得越频繁
二、本质不是“等多久”,而是“等什么”
三、核心机制拆解:Playwright 的四种高级等待模式
四、典型案例对比:同一个登录,两种写法,一个天一个地
五、工程落地启示:稳定不是跑得快,是等得准
六、问自己一个问题
一、sleep堆得越多,脚本崩得越频繁
上个月,一个朋友深夜发消息给我:“我们的Playwright用例,跑十次能绿七次就不错了。”
我让他发一段代码过来。
看到的第一行就是:time.sleep(3)
第二行:time.sleep(2)
一个登陆流程,硬等了15秒。
更糟糕的是,换到测试环境,网络快一点的时候,sleep浪费大量时间。慢的时候,3秒不够,照样挂。
这不是个例。
我翻过很多团队的自动化仓库,最常见的“等待”写法就是time.sleep。从Selenium时代带过来的习惯,到现在还没改。
为什么还在用sleep?
因为懒。因为不理解页面到底在等什么。因为觉得“多等几秒总没错”。
但现实是:
sleep是对不确定性的投降。你放弃了判断,把命运交给一个固定数字。
今天能跑,不代表明天能跑。
环境一变,全盘崩溃。
二、本质不是“等多久”,而是“等什么”
很多人没想明白一件事:
测试脚本的不稳定,90%来自“时间上的猜测”。
你猜3秒能加载完,但它用了3.1秒。
你猜动画0.5秒结束,但CPU一波动,变成0.8秒。
每一次time.sleep,本质上是在说:
“我不知道页面什么时候准备好,所以我随便猜一个数字。”
而Playwright给出的答案完全不同:
不要等时间,等条件。
条件是什么?
元素变为可见、可点击、可编辑。
某个网络请求完成。
某个JavaScript函数返回true。
页面达到特定的加载状态。
这些不是猜测,是可观测的事实。
Playwright内置的自动等待机制,核心就是轮询这些条件,直到满足或超时。
所以稳定性提升90%不是夸张。
你从“我猜1秒后按钮出现”变成了“按钮出现我才继续”。
误差从秒级降到了毫秒级,而且不受环境影响。
三、核心机制拆解:Playwright 的四种高级等待模式
先看一个总览图,搞清楚不同场景该用什么。

下面拆解每一种。
- waitForSelector - 最常用的元素级等待
不是等固定秒数,而是等某个元素达到指定状态。
错误做法
time.sleep(2)
page.locator(".toast").click()
正确做法
page.wait_for_selector(".toast", state="visible")
page.locator(".toast").click()
支持四种状态:attached(存在DOM)、detached(移除)、visible(可见)、hidden(隐藏)。
本质是:Playwright每50ms检查一次,直到条件满足或超时。
- waitForNavigation - 处理页面跳转
点击一个按钮后页面跳转,你不能等某个元素,因为整个页面会重新加载。
错误:先点击,然后sleep
page.click("a.login")
time.sleep(3)
正确:同时等待导航完成
async with page.expect_navigation():
page.click("a.login")
这里有一个容易被忽略的点:expect_navigation必须在触发跳转的动作之前设置,否则可能错过事件。
- waitForResponse - 等待后端接口返回
很多前端行为不刷新页面,但依赖API响应。等元素出现不够,因为数据还没回来。
等待某个特定API返回
with page.expect_response(lambda res: "/api/user/info" in res.url) as response_info:
page.click("#refresh-btn")
response = response_info.value
assert response.status == 200
这解决了“数据驱动UI更新”场景下的精确等待。
- waitForFunction - 终极万能钥匙
当以上都不够用时,直接在浏览器上下文执行一段JS,等它返回true。
等待某个全局变量变成true
page.wait_for_function("window.dataLoaded === true")
等待列表长度大于0
page.wait_for_function("""() => {
return document.querySelectorAll('.item').length > 5
}""")
核心在于:你把“判断逻辑”交给浏览器自己运行,不需要在测试脚本里反复获取DOM再判断。
四、典型案例对比:同一个登录,两种写法,一个天一个地
用最常见的登录后跳转到仪表盘举例。
坏味道版本(满屏sleep)
page.goto("https://example.com/login")
page.fill("#username", "test")
page.fill("#password", "pass")
page.click("button:has-text('登录')")
time.sleep(5) # 等跳转
page.click(".dashboard-widget") # 经常挂,因为页面没完全加载
time.sleep(2)
assert page.locator(".welcome").text_content() == "欢迎回来"
问题:
网络慢的时候,5秒不够,失败。
网络快的时候,浪费5秒。
仪表盘里某个widget依赖二次API调用,time.sleep(5)后还没渲染完。
稳定版本(状态等待)
page.goto("https://example.com/login")
page.fill("#username", "test")
page.fill("#password", "pass")
点击并等待导航完成
with page.expect_navigation():
page.click("button:has-text('登录')")
等待仪表盘的核心元素可见
page.wait_for_selector(".dashboard-widget", state="visible")
等待某个关键API返回(可选)
with page.expect_response("/api/dashboard/summary"):
pass
再做断言
assert page.locator(".welcome").text_content() == "欢迎回来"
两个对比:
坏味道版本:平均执行时间7-12秒,失败率约20%。
稳定版本:平均执行时间2-4秒,失败率低于1%。
这不是工具的区别,是对待等待的理解区别。
可以被截图传播的观点句:
sleep是对不确定性的投降,而等待策略是对系统行为的精确建模。
Playwright让你等的是条件,不是时间。
五、工程落地启示:稳定不是跑得快,是等得准
如果你现在还在团队里写自动化,下面三条建议直接拿去用。
启示一:全局搜time.sleep,一个不留
除了极少数场景(比如等待外部系统的非Web事件),time.sleep都应该被替换。
替换成wait_for_selector、wait_for_response或wait_for_function。
不是优化,是重构。
每删一个sleep,就消灭一个不确定性。
启示二:给每条等待设置合理超时
不要依赖默认的30秒。
登录跳转:5秒足够。
复杂图表加载:可以给10秒。
超时后明确抛出错误,而不是无限等。
page.wait_for_selector(".heavy-chart", timeout=10000)
启示三:把等待策略封装成团队规范
不要每个人都写一遍with page.expect_navigation()。
封装成click_and_wait_for_nav(locator)。
封装成wait_for_toast_message(text)。
初级工程师拿来就用,不会犯错。
中级工程师把经验沉淀成代码。
对在校生来说:
不要再背selenium的sleep写法。现在就开始练习waitForFunction和waitForResponse。这些才是工业级的思维。
六、问自己一个问题
回到你最近写过的一个自动化用例。
把所有time.sleep删掉,全部换成条件等待。
你敢不敢在下一个发版日,只靠这套代码跑全回归?
如果不敢,问题不在工具。
在你的测试设计里,有多少步骤是依赖“猜测”,而不是依赖“事实”?