【Appium 系列】第18节-重试与容错 — 移动端测试的稳定性保障

配套代码:utils/retry.pytests/test_login_api.py

说明:本节所有代码示例均来自一个真实的移动端自动化测试项目,已做模糊化处理。


为什么需要重试

移动端测试比 Web 测试更容易出现偶发性失败。以下几种情况在本地和 CI 上反复出现:

  • 网络波动 :Appium 调用 find_element 时底层发起的 HTTP 请求超时,抛出 TimeoutException: An element could not be located on the page using the given search parameters
  • 元素加载慢 :页面渲染被动画或懒加载阻塞,在元素存在前执行了 click(),触发 NoSuchElementException: An element could not be located
  • 键盘弹出延迟send_keys() 执行时输入框尚未获得焦点,驱动报 InvalidElementStateException: Element must be user-editable to sendKeys
  • 动画未结束 :页面切换动画仍在运行时点击下一个元素,抛出 ElementClickInterceptedException: Element is not clickable at point (x, y)

这些失败不是代码逻辑 bug,而是环境或时序导致的不稳定。重试 2-3 次后,绝大部分情况下用例能正常通过。utils/retry.py 提供了 retry_on_failure 装饰器和 RetryHelper 工具类来解决这类问题。


重试装饰器

utils/retry.py 中的 retry_on_failure 是一个装饰器,直接加在测试函数或页面操作方法上即可启用自动重试。

复制代码
from utils.retry import retry_on_failure

class TestLogin:

    @retry_on_failure(max_attempts=3, delay=2.0)
    def test_login_with_retry(self, driver):
        """带重试的登录测试"""
        from pages.login_page import LoginPage
        login_page = LoginPage(driver)

        login_page.input_phone_email("test_user@example.com")
        login_page.input_password("test_password")
        login_page.click_login_button()

        assert login_page.is_on_home_page()

参数说明

  • max_attempts:最大尝试次数(包括首次执行),默认 3。最后一次失败后不再重试,直接抛出异常。
  • delay:每次重试间隔,单位秒,默认 1.0。重试前通过 time.sleep(delay) 等待。
  • exceptions:需要捕获并重试的异常类型元组,默认 (Exception,) 即捕获所有异常。可以精确指定只重试特定异常,例如 exceptions=(TimeoutException, NoSuchElementException)

执行流程

  1. 首次执行函数,成功则直接返回结果。

  2. 捕获到 exceptions 中指定的异常时,记日志并等待 delay 秒后重试。

  3. 第 2~N 次重复上述流程。

  4. 所有 max_attempts 次均失败后,抛出最后一次捕获的异常。

    精确捕获特定异常,避免因 AssertionError 误重试

    @retry_on_failure(max_attempts=3, delay=2.0, exceptions=(TimeoutException, NoSuchElementException))
    def test_element_interaction(self, driver):
    ...


RetryHelper 辅助类

除了装饰器,utils/retry.py 还提供了 RetryHelper 工具类,适用于不能或不想用装饰器的场景------比如在页面对象(Page Object)的某个方法内部对单个操作做重试。

retry_operation

对任意可调用对象执行重试,适合在 Page Object 中精细控制:

复制代码
from utils.retry import RetryHelper

class LoginPage:
    def click_login_with_retry(self):
        """对单次点击操作做重试,不波及整个用例"""
        RetryHelper.retry_operation(
            self.click_login_button,
            max_attempts=3,
            delay=1.0
        )

签名:retry_operation(operation, max_attempts=3, delay=1.0, exceptions=(Exception,), *args, **kwargs)

retry_until_success

重试直到满足自定义成功条件,适合轮询等待某个状态:

复制代码
RetryHelper.retry_until_success(
    self.click_login_button,
    max_attempts=10,
    delay=0.5,
    success_condition=lambda: self.is_element_displayed("xpath", "//*[@text='首页']")
)

success_condition 是一个可调用对象,返回 True 视为成功。这对于动画未结束、数据尚在加载的场景非常实用------不是单纯等固定时间,而是等到条件成立。


重试策略选择

策略 适用场景 参数建议
固定延迟 网络波动、元素加载慢 delay=2.0
指数退避 服务端限流、资源竞争 延迟递增:2s → 4s → 8s
快速重试 键盘弹出延迟、动画未结束 delay=0.5

指数退避可以在 retry_on_failure 基础上手动实现:

复制代码
def retry_with_backoff(max_attempts=3, base_delay=1.0):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            last_exception = None
            for attempt in range(max_attempts):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    last_exception = e
                    if attempt < max_attempts - 1:
                        delay = base_delay * (2 ** attempt)
                        logger.warning(f"等待{delay}秒后重试: {e}")
                        time.sleep(delay)
            raise last_exception
        return wrapper
    return decorator

常见坑与报错

以下是在实际项目中踩过的坑,每个都附有具体报错信息。

坑 1:重试覆盖了真正的代码 bug

复制代码
AssertionError: Expected True but got False
  assert login_page.is_on_home_page()

如果你用 @retry_on_failure(max_attempts=5, delay=1.0) 不加 exceptions 参数,默认捕获 Exception,连 AssertionError 也会被兜住重试。测试报告里显示"重试后通过",但实际是业务逻辑有问题。应通过 exceptions 参数限定只重试环境类异常,让断言失败直接暴露。

坑 2:重试间隔太短,重试等于白试

复制代码
selenium.common.exceptions.TimeoutException: Message: timeout (WARNING: The server did not provide any stacktrace information)

页面上一个接口通常需要 1-3 秒返回数据,如果重试间隔设成 delay=0.3,页面还没加载完就重试,同样会再次超时。推荐至少 1.5-2 秒。

坑 3:重试次数太多,拖慢整个测试集

复制代码
Test session running in 12m 34s ...

100 个用例,每个重试 3 次 × 2 秒延迟 = 多出 600 秒(10 分钟)。如果每个用例都无脑加 @retry_on_failure,测试集时间膨胀严重。只在确实偶发的用例上加重试,能通过 find_elementWebDriverWait 解决的优先用等待。

坑 4:装饰器忘了加 @wraps,pytest 报告中函数名变成 wrapper

复制代码
tests/test_login.py::test_login_with_retry → wrapper  # 函数名被覆盖

utils/retry.py 中已经写了 from functools import wraps 并在内层函数上加 @wraps(func)。如果你自己写重试装饰器时漏了这一行,func.__name__ 会变成 wrapper,pytest 报告中看到的测试名称将是 wrapper 而非原始函数名,导致无法匹配 Allure 报告中的测试用例。

坑 5:exceptions 参数传了错误类型导致异常逃逸

复制代码
@retry_on_failure(max_attempts=3, exceptions=(TimeoutException))

元组写法少了逗号,(TimeoutException) 会被 Python 解析为单个表达式而非元组。正确的写法是 (TimeoutException,)。漏掉逗号后,重试装饰器不会捕获任何异常,偶发失败直接报错退出。

坑 6:不适合重试的场景强行加重试

复制代码
AttributeError: 'NoneType' object has no attribute 'click'
  driver = None  # driver 未初始化

driverNone、测试数据错误、配置写错------这些属于一次性失败,重试 100 次结果一样。先定位根因,不要用重试掩盖配置或数据问题。


哪些场景适合用重试

  • 元素定位超时(页面加载慢)
  • 网络请求超时(网络波动)
  • 偶发性断言失败(动画未结束,元素尚未到达预期状态)

哪些场景不适合用重试

  • 逻辑错误(代码 bug,重试多少次都一样)
  • 数据错误(测试数据不对,重试后仍然一样)
  • 配置错误(driver 没初始化、API 地址不对)

总结

重试是应对移动端测试不稳定性的实用手段,但需要搭配正确的参数和策略。utils/retry.py 提供了装饰器和工具类两套方案------装饰器适合整个测试函数级别,RetryHelper 适合细粒度控制单次操作。用好 exceptions 参数避免误吞断言失败,控制 delaymax_attempts 防止测试集膨胀。能通过显式等待(WebDriverWait + expected_conditions)解决的问题,优先用等待,重试作为兜底方案。

相关推荐
还是鼠鼠1 小时前
AI掘金头条新闻系统 (Toutiao News)-用户注册-创建用户
后端·python·mysql·fastapi·web
灰灰勇闯IT1 小时前
DeepSeek-R1 在 CANN 上的推理部署
pytorch·python·深度学习
l1t1 小时前
Hy-MT2-1.8B总结的pgvector 0.8.2解决了并行HNSW索引构建漏洞
数据库·人工智能·postgresql
太华1 小时前
学习AI Agent编程-第二天-LangGraph ReAct模式实现
人工智能
dayuOK63072 小时前
从“爆款复刻”到“个性化创作”:AI辅助写作的技术挑战与演进方向
人工智能·职场和发展·自动化·新媒体运营·媒体
Raink老师2 小时前
【AI面试临阵磨枪-58】AI 生成内容合规、版权、审核机制设计
人工智能·面试·职场和发展
lizhihai_992 小时前
股市学习心得-与英伟达核心 PCB 相关的八家关联企业
大数据·人工智能·学习
嗝o゚2 小时前
昇腾CANN ops-nn 仓的 Activation 算子:不只是 ReLU
人工智能·cann·ops-nn
thubier(段新建)2 小时前
从需求到上线:需求→业务→架构→功能→实现 全链路落地方法论
人工智能·架构