Playwright中对不稳定测试的重试处理

在实施自动化测试时,最令人头疼的可能就是遇到那些“`不稳定的测试`”

前言

在实施自动化测试时,最令人头疼的可能就是遇到那些“不稳定的测试”。这些测试在没有对代码进行任何更改的情况下,时而通过,时而失败。这种不一致性会导致对自动化测试集的不信任,并浪费大量宝贵的时间在不断调试上。

本文,我们将分享一个简单但有效的方案,用于处理 Playwright 中的不稳定测试:一个自定义的重试工具函数

何为不稳定的测试?

不稳定测试(或“抖动测试”) 是指那些并非由于代码缺陷原因而间歇性失败的测试。即便应用程序和测试代码都保持不变,它们的执行结果也可能不同。

为什么会变得不稳定?

测试不稳定的原因有很多,最常见的包括以下一些情况:

  1. 网络延迟:API 或页面资源加载缓慢,导致测试超时。
  2. 服务器响应慢:后端服务器出现过载,业务处理时间过长,会导致响应时间过长。
  3. 第三方依赖:依赖的外部服务(如支付网关或社交媒体 API)不够稳定,时好时坏。
  4. 竞态条件:测试在应用程序完全渲染或状态更新之前就尝试与元素交互。
  5. 浏览器差异:不同浏览器处理页面加载和 JavaScript 执行的方式略有不同。

一个简单的解决方案

Playwright 内置了自动重试机制,但它是针对整个测试的。

有时,如果我们只想重试测试中的某一部分,而不是从头开始执行,这时我们就可以自定义一个重试函数来处理。

该函数将接收一个操作(一个函数)作为参数,并在失败时尝试多次执行它。

定义重试装饰器

创建 retry_decorator.py 文件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# retry_decorator.py
import asyncio
import functools
from typing import Callable

def retry_async(max_retries: int = 3, delay: float = 1.0):
    """
    一个用于异步函数的重试装饰器。

    :param max_retries: 最大重试次数,默认为 3。
    :param delay: 每次重试之间的延迟(秒),默认为 1.0。
    """
    def decorator(func: Callable):
        @functools.wraps(func)
        async def wrapper(*args, **kwargs):
            last_exception = None
            for attempt in range(max_retries):
                try:
                    print(f"--- 尝试执行 {func.__name__} (Attempt {attempt + 1}/{max_retries}) ---")
                    return await func(*args, **kwargs)
                except Exception as e:
                    last_exception = e
                    print(f"尝试 {attempt + 1} 失败: {e}")
                    if attempt < max_retries - 1:
                        print(f"等待 {delay} 秒后重试...")
                        await asyncio.sleep(delay)
            
            print(f"所有 {max_retries} 次尝试均失败。")
            raise last_exception
        return wrapper
    return decorator

这个重试装饰器的核心思路:

  1. 循环 max_Retries 次。
  2. try 块中执行传入的Callable函数 。
  3. 如果成功,立即返回结果。
  4. 如果失败,捕获错误,等待一秒,然后进行下一次尝试。
  5. 如果所有尝试都失败了,抛出最后一次的错误。

在测试脚本文件中使用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# test_example_decorator.py
import pytest
from playwright.async_api import Page, expect
from retry_decorator import retry_async

# 模拟页面, 其内容会动态变化
FLAKY_PAGE_HTML = """
<!DOCTYPE html>
<html>
<head>
    <title>不稳定页面</title>
</head>
<body>
    <h1>测试页面</h1>
    <p id="status">加载中...</p>
    <script>
        setTimeout(() => {
            document.getElementById('status').innerText = '完成!';
        }, 1500);
    </script>
</body>
</html>
"""

@pytest.mark.asyncio
async def test_flaky_element_with_decorator(page: Page):
    await page.set_content(FLAKY_PAGE_HTML)
    
    # 直接调用被装饰的函数,重试逻辑被透明地处理了
    await check_status(page)
    
    print("测试通过!")


# 对业务处理方法进行装饰
@retry_async(max_retries=5, delay=1)
async def check_status(page: Page):
    """这个函数包含了不稳定的断言,现在被装饰器保护"""
    status_locator = page.locator("#status")
    await expect(status_locator).to_have_text("完成!")
    print("状态检查成功!")

方案的优缺点

优点:

  • 简单性:易于理解和实现。
  • 可控性:可以精确控制哪些操作需要重试,而不是重试整个测试。
  • 可复用性:可以在多个测试中重复使用这个装饰器。

缺点:

  • 掩盖问题:过度使用可能会掩盖真正的不稳定性根源(如性能问题或竞态条件)。
  • 增加测试时间:如果测试频繁失败,重试会显著增加总执行时间。
  • 调试困难:当测试失败时,可能不清楚是第几次尝试失败的,日志可能会变得混乱。

结语

通过这个自定义的重试工具,我们在使用 Playwright 处理歇性测试失败的不稳定测试时,提供了一个细粒度的控制层,可以在特定操作失败时进行恢复,而无需放弃整个测试。

当然,实际应用时,也需要根据被测业务的实际情况,避免滥用。最好把它当作一个临时的解决方案或针对已知、难以修复的不稳定性的补丁。

对于不稳定测试,最根本的处理,还是应该找到并解决导致不稳定的根本原因。

使用 Hugo 构建
主题 StackJimmy 设计