前言
在实施自动化测试时,最令人头疼的可能就是遇到那些“不稳定的测试”。这些测试在没有对代码进行任何更改的情况下,时而通过,时而失败。这种不一致性会导致对自动化测试集的不信任,并浪费大量宝贵的时间在不断调试上。
本文,我们将分享一个简单但有效的方案,用于处理 Playwright 中的不稳定测试:一个自定义的重试工具函数。
何为不稳定的测试?
不稳定测试(或“抖动测试”) 是指那些并非由于代码缺陷原因而间歇性失败的测试。即便应用程序和测试代码都保持不变,它们的执行结果也可能不同。
为什么会变得不稳定?
测试不稳定的原因有很多,最常见的包括以下一些情况:
- 网络延迟:API 或页面资源加载缓慢,导致测试超时。
- 服务器响应慢:后端服务器出现过载,业务处理时间过长,会导致响应时间过长。
- 第三方依赖:依赖的外部服务(如支付网关或社交媒体 API)不够稳定,时好时坏。
- 竞态条件:测试在应用程序完全渲染或状态更新之前就尝试与元素交互。
- 浏览器差异:不同浏览器处理页面加载和 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
|
这个重试装饰器的核心思路:
- 循环
max_Retries 次。 - 在
try 块中执行传入的Callable函数 。 - 如果成功,立即返回结果。
- 如果失败,捕获错误,等待一秒,然后进行下一次尝试。
- 如果所有尝试都失败了,抛出最后一次的错误。
在测试脚本文件中使用
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 处理歇性测试失败的不稳定测试时,提供了一个细粒度的控制层,可以在特定操作失败时进行恢复,而无需放弃整个测试。
当然,实际应用时,也需要根据被测业务的实际情况,避免滥用。最好把它当作一个临时的解决方案或针对已知、难以修复的不稳定性的补丁。
对于不稳定测试,最根本的处理,还是应该找到并解决导致不稳定的根本原因。