- 片状测试在相同的代码上通过和失败,没有任何变化。
- 通常的原因是时间、共享状态、不稳定的数据、外部服务和环境差异。
- 将固定睡眠替换为等待特定条件,并为每个测试提供自己的数据。
- 重试可以隐藏真正的错误。隔离不稳定的测试,在截止日期内修复它,或者删除它。
构建变红,有人点击“重新运行”,它通过了,每个人都继续前进。这看起来无害,但是片状测试慢慢地破坏了测试套件最有价值的东西:人们对它的信任。当故障通常只是噪音时,真正的错误也会被忽视。
本指南介绍了测试变得不稳定的原因以及如何正确修复每个原因。我们在 Python 3.13 中运行了计时和共享状态演示,显示的数字来自这些运行。它们是小型演示,旨在使不稳定的情况变得容易看到,因此您自己的故障率会有所不同。
什么是片状测试?
片状测试对相同的代码给出不同的结果。尽管产品中没有任何变化,但它在一次运行中通过,在下一次运行中失败。测试可能有错误,或者代码可能包含真正的间歇性错误。第二种可能性正是你不应该简单地忽略它们的原因。
为什么片状测试成本如此之高
- 失去信任。一旦人们认为红色意味着“可能不稳定”,他们就会停止调查真正的失败。
- 浪费时间。开发人员花费数小时重新运行管道并检查与他们的更改无关的故障。
- 交付速度较慢。 合并和发布会等待重新运行,从而破坏了 CI/CD 的要点。
- 隐藏的错误。 产品中的竞争条件看起来就像测试噪音,而这些正是在生产中造成损害的。
常见原因一览
| 原因 | 典型标志 | 常规修复 |
|---|---|---|
| 定时和异步 | CI 繁忙时失败更多 | 等待一个条件 |
| 共享状态 | 失败取决于测试顺序 | 隔离数据,重置状态 |
| 日期和随机性 | 在某些时间或日期失败 | 控制时钟和种子 |
| 脆性选择器 | UI更改后失败 | 使用稳定的定位器 |
| 外部服务 | 当依赖缓慢时失败 | 模拟或存根它们 |
| 环境差异 | 本地通过,CI 失败 | 与容器对齐 |
| 资源限制 | 并行运行时失败 | 调整工人,解决争用 |
原因1:定时和固定睡眠
这是最常见的原因。测试假设后台工作将在固定延迟后完成。为了查看效果,我们编写了一个小测试,其中工作最多需要 2 毫秒,而测试在检查之前只等待 1 毫秒:
def flaky_timing():
result = {}
def work():
time.sleep(random.uniform(0, 0.002))
result["done"] = True
threading.Thread(target=work).start()
time.sleep(0.001) # hope it has finished
assert result.get("done")
解决办法是等待实际情况,并设定一个慷慨的上限:
def fixed_timing():
result = threading.Event()
def work():
time.sleep(random.uniform(0, 0.002))
result.set()
threading.Thread(target=work).start()
assert result.wait(timeout=1) # continues as soon as it is ready
我们运行每个版本 200 次:
| 版本 | 失败满分 200 次 |
|---|---|
| 固定睡眠 | 102 |
| 等待一个条件 | 0 |
结果不仅仅是可靠性。固定版本在工作完成后立即移动,因此通常也更快。较长的睡眠时间将使第一个版本失败的频率降低,但每次运行都会变慢,并且在繁忙的 CI 机器上仍然可能失败。
在浏览器测试中,同样的原则也适用。使用 Playwright 的自动等待,或显式 Selenium waits,而不是 time.sleep:
# Playwright: retries until true or a timeout is reached
expect(page.get_by_text("Order placed")).to_be_visible()
# Selenium: an explicit wait for a condition
WebDriverWait(driver, 10).until(
EC.visibility_of_element_located((By.ID, "confirmation"))
)
原因 2:共享状态和测试顺序
当测试共享数据时,一个测试可以影响另一个测试,结果取决于它们运行的顺序。这两个测试使用一个共享列表:
store = []
def test_add():
store.append("x")
assert len(store) == 1
def test_empty():
assert store == []
| 订单 | test_add | test_empty |
|---|---|---|
| 清空,然后添加 | 通行证 | 通行证 |
| 添加,然后清空 | 通行证 | 失败 |
我们的运行证实了这一点,这就是为什么一个套件可以运行数月,然后在有人添加并行运行或随机排序插件时中断。治疗方法是隔离:
- 使用一个固定装置来设置和清理数据,为每个测试提供新的数据。我们的 pytest 教程 展示了如何操作。
- 创建唯一的记录,例如带有随机后缀的电子邮件,代替共享的“测试用户”。
- 将数据库测试包装在 事务 中,并在您的设置允许的情况下回滚。
- 定期以随机顺序运行测试,以便尽早发现这些问题。
原因 3:日期、时区和随机性
整个星期都通过的测试可能会在周六、午夜或该月的最后一天失败。将时间作为输入,而不是读取逻辑中的时钟:
def is_weekend(today=None):
today = today or datetime.date.today()
return today.weekday() >= 5
is_weekend(datetime.date(2026, 11, 7)) # True (a Saturday)
is_weekend(datetime.date(2026, 11, 9)) # False (a Monday)
这些测试在任何一天都会给出相同的答案。同样,在测试中为随机值设置固定种子,并在使用随机数据时记录它,以便可以重现故障。不要假设来自外部源的集合、字典或没有 ORDER BY 的数据库查询的特定顺序。
原因 4:选择器脆弱
查找带有 div:nth-child(3) > span 的按钮的测试会因任何布局调整而中断,并且仅对于某些屏幕尺寸或变体可能会失败。首选基于用户所见或专用测试属性的定位器。我们的 Playwright 定位器 指南解释了尝试它们的顺序。动画是另一个常见的麻烦来源,因此请考虑在测试环境中禁用它们。
原因 5:外部服务和网络
如果测试调用真实的支付提供商或第三方 API,那么其结果取决于其他人的正常运行时间和速率限制。模拟大多数测试的这些调用,并针对实际服务保留少量单独的合同检查,以便您仍然可以了解它是否发生变化。我们的单元测试与集成测试指南解释了如何将它们分开。
提示:“本地通过,CI失败”通常意味着速度、时区、资源或版本的差异。在本地和 CI 中的同一容器映像中运行测试可以消除其中的许多问题。我们的 Docker Compose 教程 可以提供帮助。
如何诊断片状测试
- 确认。多次运行测试,没有任何变化。使用 pytest,一个简单的 shell 循环就可以工作,并且像
pytest-repeat这样的插件可以为您提供--count=100。 - 记录证据。保存每次故障的日志、屏幕截图和痕迹。 Playwright 的跟踪查看器在这里特别有用。
- 强调条件。 使用有限的 CPU 并行或以随机顺序运行它。经常失败会让学习变得更容易。
- 单独测试。 如果它总是单独通过但在套件中失败,则怀疑共享状态。
- 寻找模式。检查涉及哪些机器、一天中的时间以及邻近的测试。
- 检查产品。 诚实地询问这是否可能是您的应用程序中真正的竞争条件。
自动重试可以修复吗?
重试是一个有用的短期安全网,但并不是治愈方法。 CI 中的单次自动重试可以在您调查时保持工作顺利进行,但需要重试才能通过的测试仍然会告诉您一些信息。更糟糕的是,真正的间歇性产品错误在 20% 的时间内失败,通常会在不被注意到的情况下重试。
使用这些规则的重试:将重试限制为一两次,记录需要重试的每个测试,并将该列表视为您的待办事项列表。报告为“重试通过”的测试应视为需要调查的问题,而不是成功。
片状测试的团队流程
- 测量。 跟踪哪些测试失败,然后通过相同的提交。许多 CI 工具和测试报告程序都可以证明这一点。
- 隔离。 将已确认的片状测试移出阻塞运行,以便它停止干扰每个人,但继续运行并跟踪它。
- 指定所有者和截止日期。 没有截止日期的隔离区只会变成墓地。
- 修复或删除。如果测试不值得修复,请将其删除,并检查其覆盖范围是否仍然存在于其他地方。
- 预防。 在合并之前多次运行新测试。每五十次运行失败一次的测试现在比六个月后更容易被捕获。
将不稳定的测试视为优先的错误,而不是背景噪音。严重性和优先级也适用于此,如 严重性与优先级 中所述。如果不稳定现象很普遍,请阅读更广泛的测试自动化错误,并考虑您是否在浏览器级别进行了太多测试。
最后的话
片状测试让人感觉是随机的,但实际上很少如此。几乎每个人都可以归结为关于时间、顺序、数据或环境的假设,但这些假设并不总是正确的。选择本周管道中最常失败的测试。运行五十次,找到假设,并永久修复它。一次真正的修复可以教会团队超过一百次的重新运行。
常见问题
什么是片状测试?
片状测试在某些运行中通过,在其他运行中失败,而无需对代码进行任何更改。原因可能在于测试、环境或产品。
是什么导致了不稳定的测试?
最常见的原因是时序假设、测试之间的共享状态、不稳定的选择器、外部服务、日期处理以及 CI 和本地机器之间的差异。
我应该删除片状测试吗?
首先尝试修复它们。如果某个测试价值低且修复成本高,则删除它就可以了,但要确保其重要的覆盖范围存在于其他地方。
我应该自动重试失败的测试吗?
作为有限的安全网,是的,但记录每次重试并进行调查。单独重试会隐藏问题,并可能让真正的间歇性错误到达生产环境。
如何找到不稳定的测试?
使用 CI 历史记录或报告工具查找在同一提交中失败并通过的测试。您还可以循环运行多次可疑测试。
继续学习
需要测试、WordPress 或 SEO 方面的帮助?
我们帮助团队交付更好的软件并发展他们的网站。告诉我们您正在做什么,我们将建议明确的下一步。
更多指南 测试、人工智能和现代开发
喜欢这篇文章吗?选择一个主题并直接跳至我们最新的教程。
人工智能幻觉解释:语言模型为何会产生幻觉以及如何减少它
人工智能幻觉解释:它们是什么,为什么语言模型产生它们,常见类型,如何通过基础和提示来减少它们,以及我们进行的简单检查。
阅读文章 →DevOps环境变量解释:.env 文件、秘密和配置正确完成
使用我们运行的 Python 示例解释环境变量:读取和设置它们、为什么每个值都是字符串、.env 文件、进程继承、秘密和验证。
阅读文章 →软件开发API 速率限制解释:算法、标头和处理 429 错误
API 速率限制通过我们运行的模拟进行了解释:令牌桶、固定窗口、滑动窗口、429 响应、速率限制标头、带回退的重试和测试限制。
阅读文章 →测试自动化Python 中的模拟:unittest.mock、补丁以及导致测试谎言的错误
使用unittest.mock 在Python 中进行模拟,经过测试:模拟和return_value、side_effect、修补使用名称的位置、spec 和autospec、模拟日期以及常见错误。
阅读文章 →软件测试代码覆盖率解释:行覆盖率与分支覆盖率以及 100% 的真正含义
用真实的coverage.py运行来解释代码覆盖率:行、分支和条件覆盖率,阅读报告,为什么100%覆盖率仍然会错过错误,以及合理的目标。
阅读文章 →数据库与 SQLSQL 中的 DELETE、TRUNCATE 和 DROP:差异、回滚以及何时使用它们
DELETE、TRUNCATE 和 DROP 通过经过测试的 SQL 进行了解释:各自删除什么、回滚、触发器、身份重置、速度,以及 PostgreSQL、MySQL、SQL Server 和 SQLite 有何不同。
阅读文章 →数据库与 SQLSQL 视图解释:创建、使用和避免陷阱(附示例)
SQL 视图通过我们运行的查询进行解释:创建视图、实时结果、隐藏列、性能、可更新和物化视图、破坏的依赖关系和最佳实践。
阅读文章 →软件开发TypeScript 接口与类型:真正的差异,使用编译器进行测试
TypeScript 接口与类型,使用真实编译器进行检查:两者都可以做什么,只有一个可以做什么,声明合并,扩展与交集以及要遵循的简单规则。
阅读文章 →软件开发Python *args 和 **kwargs 解释:灵活的函数和清晰的示例
Python *args 和 **kwargs 用我们运行的代码进行了解释:打包和解包、参数顺序、仅关键字和仅位置参数、装饰器和常见错误。
阅读文章 →软件开发Python Lambda 函数:语法、实际用途以及何时不使用它们
Python lambda 函数用我们运行的代码进行了解释:语法、使用键排序、映射和过滤器、闭包、后期绑定陷阱,以及何时普通 def 是更好的选择。
阅读文章 →人工智能什么是人工智能代理?它们是如何工作的,只需运行一个简单的代理循环
什么是人工智能代理?关于代理循环、工具、内存和护栏的简单指南,以及我们运行的一个小型 Python 循环、真正的风险以及何时简单的聊天机器人是更好的选择。
阅读文章 →DevOps监控与可观察性:真正的区别,包括日志、指标和跟踪
监控与可观察性解释:已知和未知的故障、日志、指标和跟踪、检查事件演练、黄金信号、OpenTelemetry 和有用的警报。
阅读文章 →DevOpsDocker Volumes 与 Bind Mounts:以正确的方式保留容器数据
Docker 卷与绑定挂载解释:为什么容器数据消失、命名卷、绑定挂载、tmpfs、Compose 示例、备份、权限以及何时使用它们。
阅读文章 →测试自动化TDD 与 BDD:主要区别、以两种方式构建的一个功能以及何时使用每种方式
TDD 与 BDD 的解释是通过一种双向构建的折扣功能:使用我们运行的代码进行红绿重构、Given-When-Then 场景、工具、权衡以及何时使用每种方案。
阅读文章 →软件测试用户验收测试 (UAT):流程、清单和真实示例
用户验收测试解释:谁运行它、它与 QA 有何不同、七步流程、UAT 测试用例模板、清单、签核标准和常见错误。
阅读文章 →数据库与 SQLSQL NULL 值解释:IS NULL、COALESCE、NULLIF 和要避免的陷阱
通过我们运行的查询解释了 SQL NULL 值:三值逻辑、IS NULL、COALESCE、NULLIF,聚合、连接和排序如何处理 NULL,以及它悄悄引起的错误。
阅读文章 →数据库与 SQLSQL CASE WHEN 语句:语法、实例和常见错误
SQL CASE WHEN 解释了我们运行的查询:简单和搜索表单、条件顺序、SUM 透视、COUNT 技巧、自定义排序、更新和要避免的 NULL 陷阱。
阅读文章 →软件开发什么是智威汤逊?通过工作示例解释 JSON Web 令牌
什么是智威汤逊? JSON Web Tokens 解释:这三个部分,在我们运行的代码中构建和篡改的令牌,JWT 与会话,存储,刷新令牌和安全错误。
阅读文章 →软件开发JavaScript 事件循环解释:调用堆栈、微任务和计时器以及示例
JavaScript 事件循环用我们运行的代码进行了解释:调用堆栈、任务和微任务队列、为什么 setTimeout 0 不是立即执行、阻塞、饥饿和面试测验。
阅读文章 →软件开发Python 生成器和 Yield 解释:通过真实示例节省内存
Python 生成器和yield 用我们运行的代码进行了解释:它们如何暂停和恢复、测量内存节省、管道、无限序列、yield from、发送和常见陷阱。
阅读文章 →人工智能什么是嵌入?向量和余弦相似度简单解释
什么是嵌入?关于向量、余弦相似度和语义搜索的简单语言指南,其中包含我们运行的一个小示例、常见用途、选择模型和已知限制。
阅读文章 →DevOps蓝绿部署与金丝雀部署:差异、权衡以及如何选择
蓝绿部署与金丝雀部署解释:每种部署的工作原理、成本、风险和回滚、我们测试的稳定用户分桶、数据库迁移、滚动更新以及如何选择。
阅读文章 →测试自动化不稳定的测试:造成这些问题的原因以及如何彻底解决它们
我们运行的演示解释了不稳定的测试:主要原因、如何确认和诊断、正确的时间修复、共享状态和日期以及隔离过程。
阅读文章 →软件测试负载测试与压力测试:差异、指标和 k6 示例
负载测试与压力测试的解释:各自证明什么、峰值和浸泡测试、重要的指标、为什么平均值会误导、k6 个示例以及如何计划测试。
阅读文章 →软件测试测试用例与测试场景:主要区别、示例以及何时使用它们
测试用例与测试场景的解释:清晰的定义、并排的表格、从场景到案例的密码重置功能、模板以及何时使用每个功能。
阅读文章 →数据库与 SQLSQL 查询优化:读取执行计划并修复慢速查询(带计时)
对一百万行进行 SQL 查询优化测试:读取执行计划、添加正确的索引、修复不可控制的过滤器、使用复合索引和键集分页。
阅读文章 →数据库与 SQLSQL UNION 与 UNION ALL:差异、示例和性能测试
SQL UNION 与 UNION ALL 通过我们运行的查询进行了解释:重复、都遵循的规则、真实的数据错误、测量的性能、INTERSECT 和 EXCEPT 以及何时使用它们。
阅读文章 →软件开发JavaScript Fetch API 教程:GET、POST、错误处理和超时
针对本地服务器测试的 JavaScript Fetch API 教程:GET 和 POST、为什么不抛出 404、可重用的帮助程序、查询字符串、超时、CORS 和常见错误。
阅读文章 →软件开发Python 文件处理:读取、写入、CSV、JSON 和 pathlib 以及示例
使用我们运行的代码解释了 Python 文件处理:打开模式、使用、读取大文件、CSV、JSON、pathlib、二进制文件、安全写入、错误处理和常见错误。
阅读文章 →软件开发Python 类和对象:带有实际示例的 OOP 教程
用我们运行的代码解释 Python 类和对象:__init__、方法、类与实例属性、继承、属性、dunder 方法、数据类和常见错误。
阅读文章 →人工智能什么是大语言模型?法学硕士如何运作,在没有炒作的情况下进行解释
什么是大语言模型?法学硕士用简单的语言解释:标记、嵌入、注意力、训练、温度和上下文窗口、一个微小的下一个单词演示和真正的限制。
阅读文章 →DevOps适合初学者的 Linux 命令:带有示例的基本终端指南
适合初学者的 Linux 命令,按任务分组:导航、文件、查看日志、grep、管道、权限、进程和档案,并附有实用的日志分析示例。
阅读文章 →测试自动化Selenium 与 Playwright:主要区别、代码比较以及选择
Selenium 与 Playwright 比较:架构、等待、浏览器和语言支持、调试、两种工具中的相同测试、迁移建议以及如何选择。
阅读文章 →软件测试等价划分:通过实例解释测试设计技术
等效分区解释:如何找到有效和无效分区,年龄、密码和优惠券的工作示例,我们运行的测试,以及为什么将其与边界配对。
阅读文章 →软件测试软件测试中的严重性与优先级:差异、矩阵和实例
软件测试中的严重性与优先级解释:清晰的定义、带有示例的四向矩阵、级别尺度、谁设置每个级别以及如何运行错误分类。
阅读文章 →数据库与 SQLSQL 子查询教程:类型、相关子查询和 EXISTS 以及示例
SQL 子查询教程,其中包含我们运行的查询:标量、IN、FROM 和 SELECT 子查询、相关子查询、EXISTS、NOT IN NULL 陷阱以及子查询与 JOIN 与 CTE。
阅读文章 →数据库与 SQLSQL 事务和 ACID 解释:COMMIT、ROLLBACK 和隔离级别
SQL 事务和 ACID 通过我们运行的银行转账进行了解释:COMMIT、ROLLBACK、保存点、锁、隔离级别、脏读、死锁和最佳实践。
阅读文章 →软件开发Git merge 与 rebase 的区别:示例及如何选择
用真实的 log 输出、对比表、rebase 黄金法则和安全的团队工作流,讲清 Git merge 与 rebase 的区别。
阅读文章 →软件开发Python 列表推导式:语法、示例与何时避免使用
Python 列表推导式详解:语法、过滤、if-else、嵌套循环、实测耗时,以及何时普通 for 循环更合适。
阅读文章 →数据库与 SQLSQL JOIN 详解:附示例(INNER、LEFT、RIGHT、FULL)
通过示例表和查询,一步步讲解 SQL JOIN:INNER、LEFT、RIGHT、FULL、CROSS 和 SELF,并介绍常见错误与性能技巧。
阅读文章 →