为什么你的 Selenium 测试不稳定(以及如何永久解决)

发布: (2025年12月15日 GMT+8 21:08)
7 min read
原文: Dev.to

Source: Dev.to

本文涵盖内容

  • 不稳定性问题 – 为什么 time.sleep()WebDriverWait 仍然不够
  • 导致不稳定测试的原因 – 与 UI 状态变化的竞争
  • 稳定性解决方案 – 监控 DOM、网络、动画和布局偏移
  • 一行集成 – 用 stabilize() 包装你的 driver —— 零测试改写
  • 完整诊断 – 精准了解测试被阻塞的原因

如果你使用 Selenium 超过一周,可能已经写过类似下面的代码:

driver.get("https://myapp.com/dashboard")
time.sleep(2)  # Wait for page to load
driver.find_element(By.ID, "submit-btn").click()
time.sleep(1)  # Wait for AJAX

你可能会为自己写了错误的代码感到羞愧——但也会因为“它能跑”而松一口气。直到它不再工作。直到 CI 服务器比你的机器慢 10 %,测试突然有 20 % 的概率失败。

这就是 不稳定测试 的故事,为什么会出现,以及一个叫 waitless 的库如何彻底根除它们。

不稳定性问题

考虑一个真实场景:一个 React 仪表盘,用户点击按钮后发起 API 调用,返回数据后 React 重新渲染,加载指示器消失,表格出现。整个过程大约需要 400 ms,但测试却这样写:

button = driver.find_element(By.ID, "load-data")
button.click()
table = driver.find_element(By.ID, "data-table")  # 💥 BOOM

此时表格尚未出现,Selenium 抛出 NoSuchElementException。常见的“快速修复”是:

button.click()
time.sleep(2)
table = driver.find_element(By.ID, "data-table")  # Works… usually

time.sleep() 的问题

  • 增加不必要的延迟(例如比实际需要慢 2 秒)
  • 当 API 响应时间超出预期时仍然会出现不稳定
  • 失败时没有提供任何线索

为什么传统方案不起作用

time.sleep() —— 朴素做法

固定时间睡眠并期望 UI 已就绪。

问题:时间太短 → 测试失败;时间太长 → 测试套件拖慢;没有关于实际状态的反馈。

WebDriverWait —— “正确”做法

from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

WebDriverWait(driver, 10).until(
    EC.element_to_be_clickable((By.ID, "submit-btn"))
)

它只等待 单个 元素的特定条件,忽略了:

  • 正在进行的动画遮罩层
  • 未完成的 AJAX 请求
  • 移动元素的 React 重新渲染

重试装饰器 —— 否认做法

@retry(tries=3, delay=1)
def test_dashboard():
    driver.find_element(By.ID, "submit-btn").click()

重试只能掩盖不稳定性,根本解决不了问题。

真正导致不稳定测试的原因?

在调试了数百个不稳定测试后,根本原因是 与 UI 竞争

你做了什么实际发生了什么
点击按钮框架正在修改 DOM
断言文本内容AJAX 响应仍在进行
与模态框交互CSS 过渡仍在动画中
点击导航链接布局偏移导致元素位置变化

真正要问的不是 “这个元素可点击吗?” 而是 “整个页面是否已经稳定并准备好交互?”

“稳定性” 的定义

四个关键信号表明 UI 已稳定:

  1. DOM 稳定 – 没有元素被添加、删除或修改。
    检测方式MutationObserver 监听文档根节点;记录自上次变更以来的时间。

  2. 网络空闲 – 所有 AJAX 请求已完成。
    检测方式:拦截 fetch()XMLHttpRequest;统计未完成请求数。

  3. 动画完成 – 所有 CSS 动画和过渡已结束。
    检测方式:监听 animationstartanimationendtransitionstarttransitionend 事件。

  4. 布局稳定 – 元素不再移动;没有更多布局偏移。
    检测方式:随时间跟踪交互元素的边界框位置。

架构

JavaScript 插装(在浏览器中运行)

window.__waitless__ = {
    pendingRequests: 0,
    lastMutationTime: Date.now(),
    activeAnimations: 0,

    isStable() {
        if (this.pendingRequests > 0) return false;
        if (Date.now() - this.lastMutationTime < 100) return false;
        // Add additional checks for animations and layout if needed
        return true;
    }
};

该脚本通过 execute_script() 注入,监控 DOM 变更、网络活动和动画。

Python 引擎(评估稳定性)

class StabilizationEngine:
    def wait_for_stability(self):
        """Wait until all stability signals are satisfied."""
        # Checks performed automatically:
        # ✓ DOM mutations have settled
        # ✓ Network requests completed
        # ✓ Animations finished
        # ✓ Layout is stable

引擎会循环查询浏览器状态,直至 isStable() 返回 True

魔法:一行集成

无需修改任何测试代码,只需在创建 driver 后加一行包装:

from waitless import stabilize

driver = webdriver.Chrome()
driver = stabilize(driver)  # ← 唯一需要的改动

# 现有测试保持不变
driver.find_element(By.ID, "button").click()  # 自动等待!

stabilize() 返回一个 StabilizedWebDriver,它拦截 find_element() 调用。返回的元素被包装为 StabilizedWebElement,其 click() 方法会先等待页面稳定:

class StabilizedWebElement:
    def click(self):
        self._engine.wait_for_stability()  # 自动等待!
        return self._element.click()      # 然后点击

你的测试不再感知到等待的存在,只会不再失败。

处理边缘情况

真实应用往往有持续活动(加载指示器、分析轮询、WebSocket 心跳)。waitless 提供可配置阈值。

示例:忽略无限动画

from waitless import StabilizationConfig, stabilize

config = StabilizationConfig(
    network_idle_threshold=2,      # 允许最多 2 个未完成请求
    animation_detection=False,    # 忽略旋转加载等持续动画
    strictness='relaxed'          # 仅检查 DOM 变更
)

driver = stabilize(driver, config=config)

你可以根据应用行为自行调整检测策略,而无需重写测试。

Back to Blog

相关文章

阅读更多 »