Skip to content

第 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。它会一次性打开三件套:

  1. Inspector 窗口:可视化单步执行测试;
  2. 有头浏览器(headed mode):能看到页面真实变化;
  3. 超时置零:所有自动等待不再超时,方便你慢慢观察。
bash
# 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_success

Inspector 工具栏支持 播放 / 暂停 / 单步:每执行一个动作(点击、填写),代码中对应行和页面上的目标元素都会同时高亮,你可以逐帧看清"测试到底做了什么"。

19.3 page.pause():从指定断点开始调试

如果测试很长,逐步步进太慢,可以在关心的位置插入硬断点:

python
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,现在来看怎么用它复盘一次偶发失败:

bash
# 失败用例会留下 test-results/xxx.zip 轨迹文件
playwright show-trace test-results/test-cart-test-checkout-chromium.zip

Trace Viewer 里每一页都是一个信息维度:

  • Actions:每个动作的耗时与结果,红色即失败点;悬停可见前后 DOM 快照;
  • Log:动作级详细日志(等待元素、滚动、注入事件……),能看到 Playwright 在超时前到底反复尝试了什么;
  • Network:该时刻的所有请求与响应,一眼识别"接口没回来"类竞态;
  • Console / Errors:前端 JS 报错常常才是测试失败的真正原因。

排查套路

先看 Actions 找到失败动作 → 切到 Log 看它在等什么 → 再去 Network/Console 找"谁没到位"。90% 的 flaky 都能沿这条链路定位。

19.5 等待策略纠偏:别再用 sleep 兜底

很多团队的 flaky 是被"补 sleep"治出来的:

python
# ❌ 反面教材:睡多久都不保险,还拖慢整套测试
time.sleep(3)
expect(page.get_by_text("加载完成")).to_be_visible()

正确做法是回到第 4、5 章的两板斧:

python
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)

针对特殊场景还有两个精确工具:

python
# 等待特定网络响应到达后再断言(而不是等固定秒数)
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 插件:

bash
pip install pytest-rerunfailures
pytest --reruns 2 --reruns-delay 1   # 全局:失败后最多重跑 2 次
python
import pytest

@pytest.mark.flaky(reruns=3, reruns_delay=2)
def test_known_flaky_until_bug_fixed(page):
    # 仅对个别已知问题用例做局部重试,并注明追踪单号
    ...

但请把它当作缓冲期管理工具而非解决方案,配套一个治理流程:

  1. 记录:flaky 用例打上 @pytest.mark.flaky 并关联 issue 编号;
  2. 量化:CI 报告里统计重试率,超过阈值(如 1%)就停下新功能修复它;
  3. 复盘:每次重试后才通过的用例,都按 19.4 的 Trace 流程找一次根因;
  4. 消灭:根因修复后删除 flaky 标记——重试配额是消耗品,不是永久豁免。

19.7 本章小结

  • Flaky 四大根因:动画、网络竞态、时间依赖、共享数据;
  • PWDEBUG=1 pytest -s 打开 Inspector + headed + 无超时的调试组合;
  • page.pause() 是只在调试模式生效的硬断点,配合 Pick Locator / Live editing 高效修定位器;
  • playwright show-trace 是事后复盘的第一证据,排查链路:Actions → Log → Network/Console;
  • expect 自动重试替代 sleeppytest-rerunfailures 只做缓冲,根因必须消灭。

🧪 随堂测验

点击你认为正确的选项。答错时会展示正确答案与原因解析。

1. 设置 PWDEBUG=1 运行 pytest 时,下列哪个行为不会发生?

2. 关于 page.pause(),说法正确的是?

3. 一个测试在并行执行时必挂、串行时必过,最可能属于哪类根因?

4. 关于治理 flaky,符合本章建议的做法是?

🛠️ 动手实践

  1. 给你的项目某条失败/偶发失败用例留一次 trace,用 playwright show-trace 打开,按"Actions → Log → Network"链路写一份 5 行以内的根因笔记。
  2. 在仓库里找出所有 time.sleep,逐一替换为 Web-First 断言或 expect_response,对比替换前后的总耗时与稳定性。
  3. 为套件中一个真实存在的已知 flaky 用例添加 @pytest.mark.flaky(reruns=2),并在注释里写上 issue 链接与预期移除日期。

下一章我们把视角从"测得稳"扩展到"测得好":第 20 章 · 无障碍测试与 Aria Snapshot