第 19 章 · 调试技巧与 Flaky 治理
本章目标:掌握 Playwright 官方调试工具链(Inspector、headed 模式、Trace Viewer),建立 flaky 测试的根因分析流程,让测试套件重新变得可信。
19.1 为什么 E2E 测试会"随机失败"
Flaky(不稳定)测试指同一份代码、同一条命令,时而通过时而失败。它是 E2E 测试最大的敌人:一旦团队习惯了"红了就重跑",整个测试套件就会失去公信力。常见根因可以归为四类:
| 根因类别 | 典型症状 | 对策方向 |
|---|---|---|
| 动画/过渡效果 | 元素"看不见"或位置偏移 | 等待动画结束,或 CSS 关闭动画 |
| 网络竞态 | 断言时数据还没渲染 | 用 Web-First 断言自动重试(第 5 章) |
| 时间依赖 | 午夜/月末才失败的用例 | 注入固定时钟,禁止依赖真实时间 |
| 共享数据 | 并行跑就挂,串行跑就过 | 测试间数据隔离(第 6/13 章) |
第一原则
遇到 flaky 的第一反应不应该是"加重试",而是先定位根因。重试只是止痛药——它把失败概率乘小了,但根因还在,总有一天会以更隐蔽的方式爆发。
19.2 PWDEBUG 与 Inspector:官方调试入口
Playwright 官方推荐的调试方式是设置环境变量 PWDEBUG=1。它会一次性打开三件套:
- Inspector 窗口:可视化单步执行测试;
- 有头浏览器(headed mode):能看到页面真实变化;
- 超时置零:所有自动等待不再超时,方便你慢慢观察。
# macOS / Linux:调试模式运行(配合 -s 看到 print 输出)
PWDEBUG=1 pytest -s test_login.py::test_login_success
# Windows PowerShell
$env:PWDEBUG=1; pytest -s test_login.py::test_login_successInspector 工具栏支持 播放 / 暂停 / 单步:每执行一个动作(点击、填写),代码中对应行和页面上的目标元素都会同时高亮,你可以逐帧看清"测试到底做了什么"。
19.3 page.pause():从指定断点开始调试
如果测试很长,逐步步进太慢,可以在关心的位置插入硬断点:
def test_checkout_flow(page):
page.goto("https://shop.example.com/cart")
page.get_by_role("button", name="结算").click()
# 从这里开始停下来,前面的步骤直接跑过
page.pause()
page.get_by_label("收货地址").fill("北京市朝阳区 xx 路 1 号")
page.get_by_role("button", name="提交订单").click()
expect(page.get_by_text("订单已创建")).to_be_visible()page.pause() 只在 PWDEBUG=1 时生效(普通运行会被忽略),所以可以安全地留在代码里临时调试。Inspector 停在断点时还有两个高频功能:
- Pick Locator:鼠标悬停页面任意元素,Playwright 自动生成优先使用 role/text/testid 的弹性定位器;
- Live editing:直接在 Inspector 的定位器输入框里修改表达式,匹配元素实时高亮——调好再复制回代码。
19.4 Trace Viewer:事后复盘的最佳证据
Inspector 适合"当场调",Trace 适合"事后查"。第 11 章我们配置过 --tracing retain-on-failure,现在来看怎么用它复盘一次偶发失败:
# 失败用例会留下 test-results/xxx.zip 轨迹文件
playwright show-trace test-results/test-cart-test-checkout-chromium.zipTrace Viewer 里每一页都是一个信息维度:
- Actions:每个动作的耗时与结果,红色即失败点;悬停可见前后 DOM 快照;
- Log:动作级详细日志(等待元素、滚动、注入事件……),能看到 Playwright 在超时前到底反复尝试了什么;
- Network:该时刻的所有请求与响应,一眼识别"接口没回来"类竞态;
- Console / Errors:前端 JS 报错常常才是测试失败的真正原因。
排查套路
先看 Actions 找到失败动作 → 切到 Log 看它在等什么 → 再去 Network/Console 找"谁没到位"。90% 的 flaky 都能沿这条链路定位。
19.5 等待策略纠偏:别再用 sleep 兜底
很多团队的 flaky 是被"补 sleep"治出来的:
# ❌ 反面教材:睡多久都不保险,还拖慢整套测试
time.sleep(3)
expect(page.get_by_text("加载完成")).to_be_visible()正确做法是回到第 4、5 章的两板斧:
from playwright.sync_api import expect
# ✅ 动作自动等待:click 会等到元素可见、稳定、可接收事件
page.get_by_role("tab", name="订单列表").click()
# ✅ Web-First 断言自带轮询重试,直到满足或超时
expect(page.get_by_test_id("order-row")).to_have_count(10)针对特殊场景还有两个精确工具:
# 等待特定网络响应到达后再断言(而不是等固定秒数)
with page.expect_response("**/api/orders") as resp_info:
page.get_by_role("button", name="刷新").click()
assert resp_info.value.ok
# 页面没有"加载完成"标记时,可以显式等网络请求空闲
page.wait_for_load_state("networkidle")19.6 重试的正确姿势与治理流程
pytest 生态里给失败用例加自动重试用的是 pytest-rerunfailures 插件:
pip install pytest-rerunfailures
pytest --reruns 2 --reruns-delay 1 # 全局:失败后最多重跑 2 次import pytest
@pytest.mark.flaky(reruns=3, reruns_delay=2)
def test_known_flaky_until_bug_fixed(page):
# 仅对个别已知问题用例做局部重试,并注明追踪单号
...但请把它当作缓冲期管理工具而非解决方案,配套一个治理流程:
- 记录:flaky 用例打上
@pytest.mark.flaky并关联 issue 编号; - 量化:CI 报告里统计重试率,超过阈值(如 1%)就停下新功能修复它;
- 复盘:每次重试后才通过的用例,都按 19.4 的 Trace 流程找一次根因;
- 消灭:根因修复后删除 flaky 标记——重试配额是消耗品,不是永久豁免。
19.7 本章小结
- Flaky 四大根因:动画、网络竞态、时间依赖、共享数据;
PWDEBUG=1 pytest -s打开 Inspector + headed + 无超时的调试组合;page.pause()是只在调试模式生效的硬断点,配合 Pick Locator / Live editing 高效修定位器;playwright show-trace是事后复盘的第一证据,排查链路:Actions → Log → Network/Console;- 用
expect自动重试替代sleep;pytest-rerunfailures只做缓冲,根因必须消灭。
🧪 随堂测验
点击你认为正确的选项。答错时会展示正确答案与原因解析。
1. 设置 PWDEBUG=1 运行 pytest 时,下列哪个行为不会发生?
2. 关于 page.pause(),说法正确的是?
3. 一个测试在并行执行时必挂、串行时必过,最可能属于哪类根因?
4. 关于治理 flaky,符合本章建议的做法是?
🛠️ 动手实践
- 给你的项目某条失败/偶发失败用例留一次 trace,用
playwright show-trace打开,按"Actions → Log → Network"链路写一份 5 行以内的根因笔记。 - 在仓库里找出所有
time.sleep,逐一替换为 Web-First 断言或expect_response,对比替换前后的总耗时与稳定性。 - 为套件中一个真实存在的已知 flaky 用例添加
@pytest.mark.flaky(reruns=2),并在注释里写上 issue 链接与预期移除日期。
下一章我们把视角从"测得稳"扩展到"测得好":第 20 章 · 无障碍测试与 Aria Snapshot。