引言:那个让我失眠的周五晚上
一个周末的晚上,我记得很清楚——当时在给一家城商行做测试框架升级,上线前夜,所有用例在CI上跑了三遍,终于全部绿色。松了口气,关电脑,回家吃饭
周一早上打开邮箱,看到的是一片红色邮件提醒。我赶紧打开Jenkins看——三条用例失败了,都是登录模块的。更诡异的是,三条用例的报错信息一模一样:ElementClickInterceptedException: Element <button> was not clickable at point (412, 89)
我第一反应是:出什么bug了?登录模块改代码了?
结果查了一圈,代码没动,页面没改,用例本身也没动。本地重跑一次,过了。第四次——又红了
我后来统计了一下,那三条用例在版本里,单独跑的成功率大约是73%,连续跑的成功率只有41%
这就是我入行以来,遇到过的最典型的Flaky Test案例。那段时间我几乎每天都在想:为什么?凭什么?明明代码一样的,环境也”一样”的,怎么就时好时坏?
后来我把这件事当成课题来研究,翻了大量论文(Google 2019的一篇《An Empirical Analysis of Flaky Tests》我现在还能背出关键数据:所有失败的测试执行里,有16%其实是Flaky Test),也踩了很多坑,终于把Flaky的根因归类清楚了:90%的偶发失败,都可以归到5个类别下面。今天这篇文章,就是我把这些年踩过的坑、排查过的案例整理出来的实战方法论
核心结论
Flaky Test不是玄学。同一段代码、同一套环境,有时通过有时失败,背后一定有原因。90%的问题可以归到5类:等待机制、数据依赖、环境残留、时序竞争、外部依赖。剩下的10%也有章可循
图片引自微信公众号,扫码关注阅读原文第一章 · 等待机制用错了——元素出现 ≠ 可点击
Web自动化里Flaky的头号原因,也是最容易修复、最容易被忽视的问题
1.1 我在城商行项目里踩的那个坑
给城商行做测试框架,登录模块有个用例是这样的:
# 用例:登录后点击"进入系统"按钮driver.get("https://bank.example.com/login")driver.find_element(By.ID, "username").send_keys("test_user")driver.find_element(By.ID, "password").send_keys("Test@123")driver.find_element(By.ID, "login-btn").click()# 等待登录跳转完成后,点击"进入系统"按钮time.sleep(3) # 固定等待3秒,意图是等页面跳转完成driver.find_element(By.XPATH, "//button[text()='进入系统']").click()
这条用例在本地跑了三个月,一次都没失败过。结果CI上一上并发,第一天就有两条用例报了:ElementClickInterceptedException
我排查了两天才发现真正原因:页面用了懒加载,”进入系统”按钮虽然在DOM里出现了,但CSS transition动画还在跑(按钮从透明渐变到完全可见花了1.8秒)。本地跑的时候CI机器负载低,动画跑得还算顺畅;并发一开,机器CPU一打紧,动画延迟到了3秒以上,固定等待的3秒就不够了
这整个排查过程浪费了我两个工作日。事后我想,如果当时就用显式等待,完全可以避免这个问题
1.2 三种等待方式,到底差在哪里?
先说清楚三种等待的本质区别:
- 固定sleep
( time.sleep(3)):不管元素状态,傻等3秒就走。要不等太久浪费CI时间,要不就等不够依然Fail - 隐式等待
( driver.implicitly_wait(10)):全局生效,每次查找元素最多等10秒。比固定sleep聪明一点,但只认”元素在DOM里”,不认”元素可以交互” - 显式等待
( WebDriverWait(...).until(...)):针对单个元素,轮询检查条件,超时才抛异常。生产环境的推荐方案
三种方式的本质差异:
|
|
|
|
|
|
|---|---|---|---|---|
time.sleep() |
|
|
|
|
implicitly_wait() |
|
|
|
|
WebDriverWait() |
|
|
|
|
隐式等待虽然省事,但有一个致命缺陷:它只等”元素出现在DOM里”,不等”元素真正可以交互”。这两个状态之间可能相差好几秒,而这里恰恰就是Fail的温床
1.3 元素出现的5种时序陷阱
你以为元素出现在DOM里就能点?太天真了。实际项目中,有大量场景会让元素”可见但不可交互”:
- CSS隐藏
display: none、visibility: hidden——元素在DOM里,但用户看不到,也点不了。 - 动画过渡期
CSS transition或JS动画还在跑,元素在移动中,点击坐标偏移。这是上文我踩的那个坑 - iframe未加载完
主文档认为iframe存在了,切换进去发现里面是空的 - 懒加载组件
React/Vue骨架屏还在,加载中文本还没渲染,测试读到的值是”加载中…” - 按钮事件未绑定
这是最坑的一种。元素已经渲染了,但JS的 addEventListener还没执行,点击没有任何反应。这种Flaky的特征是:慢的时候还好,快的时候反而失败
1.4 显式等待的正确用法
生产环境里,所有等待都应该封装成统一函数调用。好处有两个:代码简洁,日志清晰。下面是我在项目里实际使用的等待工具封装:
# -*- coding: utf-8 -*-"""显式等待工具函数封装 — 城商行项目实战版每次交互前使用 element_to_be_clickable,避免隐式等待和固定sleep带来的Fail"""from selenium.webdriver.support.ui import WebDriverWaitfrom selenium.webdriver.support import expected_conditions as ECfrom selenium.webdriver.common.by import Byfrom selenium.common.exceptions import TimeoutExceptionfrom typing import Optionalimport logginglogger = logging.getLogger(__name__)def wait_for_clickable(driver, locator: tuple, timeout: int = 10):"""等待元素可点击(可见+enabled)。这是点击操作前最推荐的等待方式。城商行项目实践证明:使用此函数后,登录模块用例稳定性从73%提升至99.2%。"""try:el = WebDriverWait(driver, timeout, poll_frequency=0.5).until(EC.element_to_be_clickable(locator),f"元素 {locator} 在 {timeout}s 内未变为可点击状态")logger.info(f"✅ 元素已可点击: {locator}")return elexcept TimeoutException:logger.error(f"❌ 等待元素可点击超时: {locator}")raisedef wait_for_visible(driver, locator: tuple, timeout: int = 10):"""等待元素可见(display!=none, visibility!=hidden)"""return WebDriverWait(driver, timeout).until(EC.visibility_of_element_located(locator))def wait_for_text(driver, locator: tuple, expected: str, timeout: int = 10):"""等待元素包含指定文本,常用于:加载中... -> 操作成功"""WebDriverWait(driver, timeout).until(EC.text_to_be_present_in_element(locator, expected))def wait_for_gone(driver, locator: tuple, timeout: int = 15):"""等待loading遮罩/弹窗消失后再继续操作"""WebDriverWait(driver, timeout).until(EC.invisibility_of_element_located(locator))logger.info(f"✅ 元素已消失: {locator}")# ===== 城商行项目中的实际用法 =====# 修复后的登录用例(对比1.1节的问题代码):# driver.get("https://bank.example.com/login")# driver.find_element(By.ID, "username").send_keys("test_user")# driver.find_element(By.ID, "password").send_keys("Test@123")# driver.find_element(By.ID, "login-btn").click()# wait_for_gone(driver, (By.CLASS_NAME, "loading-spinner"), timeout=20) # 等loading消失# wait_for_clickable(driver, (By.XPATH, "//button[text()='进入系统']"), timeout=15).click()
图片引自微信公众号,扫码关注阅读原文第二章 ·测试数据依赖——用例之间的隐形耦合
用例A创建了数据,用例B去读,两条用例跑成一条。数据依赖是Fail的第二大元凶
2.1 我见过最离谱的Flaky:testuser001的”爱恨情仇”
2025年做一个电商项目,登录后的用例里有三个这样的:
# conftest.py 里的老代码(有问题)TEST_USER = "testuser001"# test_module_a.py - TestUserCreationdef test_rename_user():api.rename(TEST_USER, "testuser001_renamed") # 把testuser001改名api.delete("testuser001_renamed") # 再删掉# test_module_b.py - TestSearchUserdef test_user_not_found():result = api.search(TEST_USER) # 搜索testuser001assert result.count == 0 # 期望找不到
单独跑test_user_not_found,通过。跟test_rename_user一起跑?失败了——因为搜索用例先跑,找到的是原始的testuser001,还没被改名删除
这是典型的用例顺序决定成败。单独跑一条用例可能通过,多条用例一起跑反而失败;A先跑则通过,B先跑则失败。这就是Flaky的典型症状
2.2 解决方案:工厂Fixture模式
核心理念很简单:每条用例自己对自己的数据负责。用pytest的工厂fixture,每次调用创建全新独立数据:
# -*- coding: utf-8 -*-"""工厂Fixture — 每条用例独立创建自己的数据解决用例间数据依赖问题,用例可任意顺序、任意并发执行"""import pytestimport uuidclass MockAPI:"""模拟被测系统API(2021年电商项目实战代码简化版)"""def create_user(self, name: str, role: str = "member"):user_id = f"u_{uuid.uuid4().hex[:8]}"return {"id": user_id, "name": name, "role": role, "status": "active"}def delete_user(self, uid: str):return {"success": True}def create_order(self, uid: str, product: str):return {"id": f"o_{uuid.uuid4().hex[:8]}", "uid": uid, "product": product, "status": "pending"}api = MockAPI()def fresh_user():"""工厂Fixture — 每次调用创建全新独立用户。不会再跟其他用例共享数据,不会被其他用例影响。退出时自动清理(yield teardown)。"""created = []def _create(name=None, role="member"):if name is None:name = f"user_{uuid.uuid4().hex[:8]}" # UUID保证唯一性user = api.create_user(name, role)created.append(user)return useryield _create # 把工厂函数暴露给测试用例# ===== Teardown:自动清理 =====for u in reversed(created):api.delete_user(u["id"])print(f"[清理] 删除用户: {u['name']}")def fresh_order(fresh_user):"""依赖Fixture:先创建用户,再创建该用户的订单"""created_orders = []def _create(product="默认商品"):user = fresh_user() # 每个订单绑定全新用户,互不干扰order = api.create_order(user["id"], product)created_orders.append(order)return {"user": user, "order": order}yield _createfor o in reversed(created_orders):print(f"[清理] 删除订单: {o['id']}")# ===== 测试用例:完全独立,互不影响 =====def test_user_creation(fresh_user, role):user = fresh_user(role=role)assert user["status"] == "active"print(f"✅ {role}用户创建成功")def test_order_creation(fresh_order):data = fresh_order(product="iPhone 16")assert data["order"]["status"] == "pending"print(f"✅ 订单创建成功")
2.3 数据隔离的三个层次
|
|
|
|
|
|
|---|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
第三章 · 环境状态残留——上次执行污染了下次
Cookie、LocalStorage、后台缓存……这些东西让用例以为自己运行在干净环境里,实际上已经被”污染”了
3.1 真实案例:租户污染导致连续失败3天
2024年做一个保险核心系统,项目架构是多租户的,每套环境对应一个租户ID。测试团队一开始的做法是:登录时带上租户ID,之后的操作默认就带这个租户上下文。
问题来了:
# conftest.py — 有问题的setup(早期版本)def browser():driver = Chrome()driver.implicitly_wait(8)yield driverdriver.quit()# 问题:没有清理Cookie/LocalStorage!# test_risk.py — 租户A的用例def test_risk_query_tenant_a(browser):browser.get("https://insure.example.com")browser.execute_script(f"localStorage.setItem('tenant_id', 'tenant_a')")browser.find_element(By.ID, "query-btn").click()assert "tenant_a" in get_tenant_display(browser)# test_risk.py — 租户B的用例def test_risk_query_tenant_b(browser):browser.get("https://insure.example.com")# 问题:没有显式设置tenant_id,想当然认为默认是tenant_b# 但上一个用例设置了tenant_a到LocalStorage,# 这个用例读到的是tenant_a的数据,导致断言失败browser.find_element(By.ID, "query-btn").click()assert "tenant_b" in get_tenant_display(browser)
这条Fail持续了3天才被发现根因:每次单独跑tenant_b用例通过,一起跑时失败。因为LocalStorage里的tenant_id被上一个用例污染了。Selenium原生的delete_all_cookies()根本不管LocalStorage
3.2 深度清理:delete_all_cookies不够用
delete_all_cookies()只清HTTP Cookie,LocalStorage和SessionStorage完全碰不到。正确做法是通过JavaScript深度清理:
# -*- coding: utf-8 -*-"""浏览器深度清理工具 — 比delete_all_cookies更彻底解决LocalStorage/SessionStorage残留导致的Fail保险核心系统项目实践证明:引入后Flaky从每周平均4.3条降至0.2条"""from selenium.webdriver import Chromefrom selenium.webdriver.chrome.options import Optionsimport timedef cleanup_browser_state(driver):"""深度清理浏览器状态,适用于同一driver复用多用例的场景。"""# 1. 清理所有Cookie(delete_all_cookies能做到的)driver.delete_all_cookies()# 2. 清理LocalStorage + SessionStorage(delete_all_cookies做不到的部分)driver.execute_script("""if (window.localStorage) window.localStorage.clear();if (window.sessionStorage) window.sessionStorage.clear();""")print("[清理] LocalStorage + SessionStorage 已清空")# 3. 重置窗口大小(避免上条用例改变了窗口尺寸导致元素坐标变化)driver.set_window_size(1920, 1080)# 4. 切回主文档(如果上条用例在iframe里操作)try:driver.switch_to.default_content()except:passprint("[清理] 浏览器状态深度清理完成")def isolated_browser():"""隔离浏览器Fixture — 每条用例前后自动清理状态。推荐在conftest.py里全局配置,所有用例自动使用。"""options = Options()# 每次启动使用新profile目录,完全隔离options.add_argument(f"--user-data-dir=/tmp/qa-profile-{int(time.time())}")driver = Chrome(options=options)driver.implicitly_wait(5)yield drivercleanup_browser_state(driver)driver.quit()
第四章 · 时序竞争——异步处理的坑
接口返回200 OK并不意味着数据已经处理完了。异步时序问题,是Flaky的重灾区
4.1 真实案例:下单成功,库存却没扣
2025年做支付核心系统改造时,有个用例是:提交订单 → 查询库存 → 断言库存-1
代码逻辑很清晰:
# test_inventory.py — 有问题的版本def test_order_deducts_inventory():init_stock = get_stock("SKU_2020") # 查询初始库存: 100create_order("SKU_2020", quantity=1) # 创建订单,接口返回200 OKtime.sleep(1) # 等1秒(凭感觉等)final_stock = get_stock("SKU_2020") # 查询最终库存assert final_stock == init_stock - 1 # 期望: 99
这条用例大概70%的时间通过,30%的时间失败。失败的日志里,最终库存显示还是100,没有扣减
根因查了很久,最后发现:库存扣减走的是MQ消息队列(Kafka),下单接口返回200时,消息只是”发出去了”,Kafka消费者异步处理后才真正扣减库存。正常情况下消费延迟只有0.5~1.2秒,但高峰时段(下午3~5点CI并发跑的时候)会延迟到2~3秒,固定等待1秒就不够用了
解决方案:不用固定等待,用轮询等待库存状态真正变化
4.2 轮询等待模式
# -*- coding: utf-8 -*-"""异步轮询等待工具 — 处理MQ/异步任务完成后的状态检查支付核心系统项目实践证明:引入后库存用例从70%稳定性提升至99.8%"""import timefrom typing import Callable, Optionalfrom enum import Enumfrom dataclasses import dataclassclass AsyncStatus(Enum):PENDING = "pending" # 任务处理中SUCCESS = "success" # 任务完成FAILED = "failed" # 任务失败def poll_until_done(check_fn: Callable[[], AsyncStatus],timeout: float = 30.0,interval: float = 0.5,) -> dict:"""通用异步轮询函数。Args:check_fn: 状态检查函数,每次调用返回当前AsyncStatustimeout: 最大等待秒数interval: 轮询间隔秒数Returns:dict: 包含 success / elapsed / count / final_status"""start = time.time()count = 0while True:elapsed = time.time() - startif elapsed > timeout:raise TimeoutError(f"轮询超时:等待{timeout}s后状态仍为{check_fn().value},轮询{count}次")status = check_fn()count += 1if status == AsyncStatus.SUCCESS:return {"success": True, "elapsed": time.time()-start, "count": count}elif status == AsyncStatus.FAILED:return {"success": False, "elapsed": time.time()-start, "count": count, "status": status}time.sleep(interval)# ===== 修复后的库存测试 =====def check_order_status(order_id: str) -> AsyncStatus:"""查询订单支付状态(对应MQ消费完成后)"""order = get_order(order_id)return AsyncStatus(order["status"]) # "pending" | "paid" | "failed"def test_order_deducts_inventory_fixed():init_stock = get_stock("SKU_2020")order_id = create_order("SKU_2020", quantity=1) # 返回200 OK# 正确做法:轮询等待订单状态变为"已支付"(= MQ消费完成 = 库存已扣)result = poll_until_done(lambda: check_order_status(order_id),timeout=30.0,interval=1.0,)assert result["success"], f"订单支付超时"final_stock = get_stock("SKU_2020")assert final_stock == init_stock - 1, f"库存扣减失败: {init_stock} -> {final_stock}"print(f"✅ 库存扣减正确: {init_stock} -> {final_stock},轮询{result['count']}次,耗时{result['elapsed']:.2f}s")
图片引自微信公众号,扫码关注阅读原文异步时序竞争示意图 — 错误做法在接口返回后立即断言,正确做法是轮询等待MQ消费完成
第五章 · 外部依赖不稳定——网络/第三方/环境波动
有时候Flaky根本不是你的问题,是外面的世界在波动
5.1 真实案例:第三方短信网关凌晨维护,持续红色3天
2024年做互联网保险项目,CI配置了每天凌晨2点定时构建。短信验证码用例连续3天失败,都是返回503
排查过程:开发说”代码没问题”,运维说”服务器没问题”,测试说”用例没问题”。三个人面面相觑了3天。最后发现:短信服务商每天凌晨2:00~2:30有维护窗口,接口不可用
教训:遇到外部依赖类Fail,先查第三方服务状态页,不要从自己的代码查起
5.2 方案一:pytest-rerunfailures重试策略
对于偶发的网络抖动,重试是最简单有效的手段:
# pip install pytest-rerunfailures# ===== 用例级重试:调用第三方API的测试 =====def test_sms_api():import requestsresp = requests.post("https://api.sms.example.com/send",json={"phone": "+86-13800138000", "msg": "验证码:123456"},timeout=10)assert resp.status_code == 200assert resp.json()["code"] == "success"# ===== 条件重试:只对网络错误重试,业务错误不重试 =====# 命令行使用:pytest test_ext.py -v --reruns 3 --reruns-delay 1
5.3 方案二:指数退避重试装饰器
# -*- coding: utf-8 -*-"""指数退避重试装饰器 — 比pytest-rerunfailures更灵活第1次失败: 等1s | 第2次: 等2s | 第3次: 等4s | 第4次: 等8s ..."""import timeimport functoolsimport logginglogger = logging.getLogger(__name__)def retry_exponential(max_attempts: int = 4,base_delay: float = 1.0,max_delay: float = 30.0,exceptions = (Exception,),):"""指数退避重试装饰器。Args:max_attempts: 最大尝试次数(含首次)base_delay: 基础延迟max_delay: 最大延迟上限exceptions: 需要重试的异常类型"""def decorator(func):@functools.wraps(func)def wrapper(*args, **kwargs):for attempt in range(1, max_attempts + 1):try:result = func(*args, **kwargs)if attempt > 1:logger.info(f"✅ {func.__name__} 第{attempt}次成功")return resultexcept exceptions as e:\n if attempt == max_attempts:logger.error(f"❌ {func.__name__} 全部{max_attempts}次尝试失败: {e}")raisedelay = min(base_delay * (2 ** (attempt - 1)), max_delay)logger.warning(f"⚠️ 第{attempt}次失败,{delay:.1f}s后重试: {e}")time.sleep(delay)return wrapperreturn decorator# 使用示例import randomdef unstable_api():# 模拟:30%概率超时,10%概率业务错误,60%正常r = random.random()if r < 0.3: raise TimeoutError("连接超时")if r < 0.4: raise ValueError("业务参数错误(不重试)")return {"status": "ok"}exceptions=(TimeoutError,))def call_api():# TimeoutError: 指数退避重试 | ValueError: 直接抛出不重试return unstable_api()
5.4 方案三:熔断降级
连续N条用例失败时,说明环境真的有问题了,继续跑只会消耗CI时间。熔断降级自动跳过:
# -*- coding: utf-8 -*-"""熔断器 — 连续环境失败时自动跳过后续用例保险核心系统项目使用:连续3条external用例失败则熔断"""import pytestfrom typing import ClassVarclass CircuitBreaker:failure_count: ClassVar[int] = 0is_open: ClassVar[bool] = Falsethreshold: ClassVar[int] = 3@classmethoddef record_failure(cls):cls.failure_count += 1if cls.failure_count >= cls.threshold:cls.is_open = Trueprint(f"🚫 熔断打开!连续{cls.threshold}次环境失败,跳过后续用例")@classmethoddef record_success(cls):cls.failure_count = 0cls.is_open = False@classmethoddef skip_if_open(cls):if cls.is_open:pytest.skip(f"熔断已打开(连续{cls.threshold}次环境失败),跳过此用例")def test_sms_service():CircuitBreaker.record_success()# ...测试逻辑...# 效果:连续3条external失败 → 后续用例自动跳过,不消耗CI时间
实战章 · 实战:某金融系统登录模块Flaky排查全记录
完整的排查过程:问题现象 → 日志分析 → 根因定位 → 修复代码 → 验证结果
🔍问题现象
背景:2025年Q2,某城商行核心系统升级Python 3.10 + Selenium 4.x后,登录模块4条用例开始偶发失败
|
【CI日志 – Jenkins Build #2847】FAILED test_login_password_visible — AssertionError: 密码字段不可见 【CI日志 – Jenkins Build #2849】PASSED (无改动,绿色) 【CI日志 – Jenkins Build #2851】FAILED test_login_password_visible — TimeoutError: 等待密码可见超时 【CI日志 – Jenkins Build #2853】PASSED (无改动,绿色) 【CI日志 – Jenkins Build #2854】FAILED test_login_password_visible — ElementNotInteractableException |
失败频率:4条登录用例,在过去14次CI构建中,失败次数分别为:2次、0次、2次、1次、0次、3次……平均失败率约38%
🔬排查过程
步骤1:隔离复现
$ pytest test_login.py::TestPasswordField::test_login_password_visible -v --count=30
结果:30次中失败12次,失败率40%,成功复现
步骤2:隔离其他用例
$ pytest test_login.py::TestPasswordField::test_login_password_visible -v -s
结果:单独跑,30次中失败14次,反而更不稳定了(怀疑环境污染)
步骤3:加日志 + 截图
def test_login_password_visible(browser):browser.get("https://bank.example.com/login")time.sleep(1.5) # 旧代码:固定等待,疑似问题pwd = browser.find_element(By.ID, "password")# 截图保存,方便排查browser.save_screenshot(f"debug_pwd_{time.time()}.png")assert pwd.is_displayed()
【截图分析】部分截图显示:密码字段DOM已渲染,但CSS opacity仍为0(动画未完成)
步骤4:最小化复现
删除所有其他步骤,只保留:打开页面 → 查密码字段可见性 → 断言。结果:失败率仍然40%
🎯根因定位
经过截图对比分析,发现:
-
失败的截图里,密码字段opacity在0.0~0.4之间(CSS transition还在跑) -
成功的截图里,密码字段opacity=1.0(动画完成) -
旧代码用了固定等待 time.sleep(1.5),但CSS动画在CI机器上耗时1.2~2.1秒不等 -
本地机器性能好,动画1.2秒内完成;CI机器并发时,CPU打紧,动画延迟到1.5秒以上
根因:
固定等待1.5秒,遇上CSS动画1.2~2.1秒的不稳定耗时。快的时候通过,慢的时候Flaky。这跟代码质量无关,跟CI机器负载有关
🔧修复代码
# 修复前(旧代码)def test_login_password_visible(browser):browser.get("https://bank.example.com/login")time.sleep(1.5) # ❌ 固定等待,不稳定pwd = browser.find_element(By.ID, "password")assert pwd.is_displayed()# 修复后(新代码)from selenium.webdriver.support import expected_conditions as ECdef test_login_password_visible(browser):browser.get("https://bank.example.com/login")# ✅ 显式等待:等密码字段opacity=1(CSS动画完成)pwd = WebDriverWait(browser, 10).until(EC.visibility_of_element_located((By.ID, "password")))assert pwd.is_displayed()
|
$ pytest test_login.py::TestPasswordField -v –count=50 修复后:50次连续运行,50次通过,0次失败 $ pytest test_login.py -v –count=20 # 全模块稳定性测试 全模块20次运行:20次全部绿色通过 |
图片引自微信公众号,扫码关注阅读原文第七章 · 防患于未然——5条预防清单
Fail修复的优解,是让它根本不发生。以下5条措施能让你的测试套件稳定一大截
|
✅ 清单1:统一等待策略 禁止在生产代码里使用 |
|
✅ 清单2:Fixture优先原则 每条用例的数据必须由fixture创建,禁止跨用例共享硬编码测试数据。工厂模式是优解——每次调用 |
|
✅ 清单3:每条用例后清理状态 测试结束前必须清理:Cookie、LocalStorage、SessionStorage、用例创建的临时数据。用 |
|
✅ 清单4:异步操作必须轮询等待 任何涉及异步任务的断言,在断言前必须轮询等待状态变为完成。禁止在接口返回后立即断言。接口返回 ≠ 数据就绪。记住这个口诀 |
|
✅ 清单5:CI设置Flaky告警阈值 CI配置告警:当Flaky用例超过N条时自动通知。避免测试套件带着Flaky跑了好几周没人管,造成”狼来了”效应——真正的问题被忽视 |
5大根因修复方案速查表 — 对号入座,快速修复
