这是鼎叔的第二十八篇原创文章。
行业大牛和刚毕业的小白,都可以进来聊聊。
欢迎关注本人专栏和微信公众号《敏捷测试转型》,大量原创思考文章陆续推出。
鼎叔第一篇自行翻译的硅谷公司技术文章,来自 facebook(Meta),关于flaky test的量化度量与应用,这也是国内很多大学教授喜欢的研究课题。Flaky test,有些人翻译成不可靠测试,形容有时通过,有时不通过,没有明确的失效重现概率。本文我也翻译成 “脆弱测试” ,读着更顺一些。
原文:How do you test your tests?
作者:Mateusz Machalica, Wojtek Chmiel, Stanislaw Swierc, Ruslan Sakevych
翻译:鼎叔
随着工程师为我们的应用开发新功能和优化,Facebook(脸书,目前改名为 Meta)的代码库每天都在变化。如果不对其进行测试验证,这些变化中的每一个都可能破坏我们产品的功能或可靠性,影响全世界数十亿用户。为了降低这种风险,我们维护了一套庞大的自动回归测试集,涵盖了各种产品和应用程序。这些测试在开发过程的每个阶段都会运行,帮助工程师尽早发现回归,防止我们的产品被影响。
虽然我们使用自动化回归测试来检测产品质量,但直到如今,我们还无法自动检测测试本身是否在恶化。自动测试是一种特别的软件,随着代码库的发展,它可能会变得不可靠。不可靠的测试(也称为脆弱的测试,flaky test)会产生虚假或不确定的信号,从而破坏工程师的信任,影响整个回归测试过程的有效性。如果测试有时通过,有时失败,而且底层产品或应用程序没有进行过任何更改,这将会迫使工程师花时间寻找可能根本不存在的问题。这种情形破坏了他们对测试过程的信任。因此,最重要的是确定测试是否变得不可靠(flaky)。
迄今为止,学术研究主要集中在试图确定哪些测试是不可靠的,哪些是可靠的。然而,软件工程实践表明,所有真实世界的测试在某种程度上都是不可靠的,即使它们是按照最佳工程原则来实现的。因此,正确的问题不是某个测试是否脆弱(flaky),而是它有多脆弱 (flaky)。为了回答这个问题,我们开发了一种测试脆弱度的度量方法:脆弱度概率分(PFS)。使用此度量,我们现在可以测试你的测试,以度量和监控其可靠性,从而能够对测试套件质量的任何倒退做出快速反应。
PFS 允许我们量化 Meta 上每个单独测试任务的脆弱程度,并监测其可靠性随时间的变化。如果我们检测到在创建后不久就变得不可靠的特定测试任务,我们就能引导工程师去修复它们。这个分数是通用的,意味着它适用于每个回归测试,不管其使用的编程语言或测试框架如何。除此之外,它能够被高效地运算,这使我们可以扩展它,从而实时监控数百万测试任务的可靠性。这种脆弱度测试的度量方法对于普通工程师来说是可以被解释和理解的。因此,许多 Meta 团队都采用 PFS 来设定测试可靠性目标,并跨团队努力推动修复不稳定的测试。
为什么使用 PFS?
当测试不稳定时,工程师会很快学会忽略它,并最终删除它,这会增加未来因代码更改导致的功能或质量倒退的风险。几年前,我们创造了一种使用机器学习预测在特定代码变更上应该运行哪些测试的方法。当时,我们意识到小型的、有针对性的测试可能有点不稳定,但这是可以容忍的,而且可以通过重试或修改测试来轻松解决。
然而,当我们进行更大的端到端测试时,可靠性是一个更大的挑战。多次重试非常耗时,而修改测试更是复杂得多。当代码发生变化时,我们需要一个非常可靠的信号,这样开发者就不会浪费时间去寻找那些实际上不存在的问题。在那时,我们假设一个真正可靠的测试永远不会在没有回退的地方显示回退。为了知道哪些测试是真正可靠的,我们需要一种自动化的方法来 “测试” 这些测试。
我们开始寻找不显示任何不可靠行为(譬如有时通过,有时不通过)的测试。我们很快意识到,所有的端到端测试都有一定程度的不可靠性:总有一些事情可能会出错,从而影响测试的可靠性。根据各种因素,昨天可靠的测试今天可能不可靠。这意味着我们还需要持续监控我们测试的可靠性,当测试的脆弱性增加到超过可容忍范围时,我们会发出警报。
最终,我们的 PFS 目标不是断言任何测试都是 100% 可靠的,因为这是不现实的。我们的目标是简单地断言一个测试是否足够可靠,并提供一个尺度来说明哪些测试不如预期中的可靠。
PFS 为什么有效?
从 30000 英尺高的视角
在没有任何额外信息的情况下,我们必须从表面上获取单个测试执行的结果——我们没有依据来判断该结果是脆弱性测试的症状还是测试检测到回归的合法指标。然而,如果我们多次执行一个特定的测试,它都表现出恒定程度的脆弱性,我们可以观察到,结果的具体分布是符合一定心理预期的。
为了测量任何测试的脆弱性,我们开发了一个统计模型,该模型产生的结果分布,类似于在多次执行显示已知脆弱程度的测试时观察到的结果分布。我们已经用概率编程语言 Stan 实现了该模型。
只要我们设置了一个假想测试的脆弱程度,该模型就允许我们生成测试结果的分布。当使用该模型时,我们已经知道一个具体的现实世界测试的最新测试结果,并想要估算该测试显示的脆弱程度。允许我们反转模型的过程称为贝叶斯推理。概率编程语言运行时非常有效地实现了它,因此我们不需要太担心。
什么会影响测试结果
为了更好地理解我们的统计(定量)模型,我们将首先考虑一个更简单、定性的模型。这个简单模型分离出三个影响测试结果的大因素:
测试是否失败肯定取决于被测代码和定义测试本身的代码。测试结果对代码版本的依赖性是必然的。一个测试完全可能有无效的断言,尽管这样的测试通常会很快被识别、修复或删除。
许多全面的、更多的端到端测试也依赖于在生产中部署的服务来正确运行。它们中的一些甚至会检查被测代码与这些服务的兼容性。正如被测代码的行为取决于特性开关和各种配置一样,测试该代码的结果也是如此。因此,测试结果对外部环境状况的依赖性通常是不可避免的,有时也是必须的。
最后,我们有一个垃圾桶,任何其他影响测试结果的因素都属于这一类。特别是存在许多影响测试执行的不确定因素,例如竞争条件、随机数的使用或网络对话时的虚假失败。我们称之为垃圾箱脆弱度,这正是我们想要度量和理解的。
在上述定性模型中,测试结果对最后一个因素的敏感性程度构成了我们对脆弱度的测量。从概念上讲,如果测试结果是可微分的,我们可以将脆弱度评分记录如下。
不幸的是,测试结果是二进制的,我们无法计算上述偏导数。我们必须找到另一种方法来量化特定测试结果对脆弱度因子的敏感性。
测试结果的不对称性
在我们早期构建统计模型的尝试中,我们偶然发现了一个有趣的问题:在替换通过和失败的测试结果方面,基础数学是对称的。根据我们自己的软件工程经验,我们知道这反映了开发人员如何看待测试脆弱性。我们进行了以下实证观察:
一个通过的测试表明没有相应的回归,而失败只是提示再次运行测试。
在如何处理通过和失败的测试结果方面的这种不对称是软件开发的一个特点。虽然工程师倾向于信任通过的测试结果,但他们通常会在同一版本的代码上多次重试失败的测试,并认为通过测试后再出现的失败是不可靠的。我们没有很好的理论解释为什么这种行为盛行。
结合上述观察结果,我们可以写出测试脆弱性的统计度量,即脆弱度概率分,如下所示。
直观地说,这个分数衡量了测试被重试任意次数后,发生失败的可能性,假设它在相同版本代码和相同环境状态下曾经测试通过。根据我们的经验观察,如果一个测试可以在任意次数的重试后能通过验证,那么在同一版本的代码和环境状态上观察到的任何失败都必须被认为是脆弱的。
在上述公式中,我们使用了条件概率,它获取了特定事件(测试失败)的发生概率,假定已满足前提条件(任意能观察到的脆弱性故障)。
统计模型
为了设计我们的统计模型,我们假设 PFS 是每个测试的固有属性——换句话说,每个测试都有一个被烘烤出来的数字,我们的目标是根据观察到的测试结果序列对其进行估计。在我们的模型中,每个测试都是被两个(而非一个)这样的数字参数化:
1 不良状态的概率,衡量由于被测代码的版本或外部环境状态而导致测试失败的频率。
2 良好状态下的失效概率,等于 PFS。
该模型不会帮助我们预测特定测试的未来结果,也不会告诉我们,特定故障是由代码版本、外部环境状态还是脆弱性引起的。然而,它确实可以帮助我们评估,在给定的测试结果序列下,该测试的脆弱程度在概率上是什么水平。
我们可以在一个非常简单的例子中看到模型的实际应用。考虑一个特定的测试,该测试已经在代码 c 的一个版本上运行,并且处于外部环境状态 W1。如果第一次尝试通过,我们就完成了,根据观察到的测试结果不对称性,我们不会重试已通过执行的测试。但是,如果第一次尝试失败,我们会重试一次测试。我们将第二次尝试视为测试的最终结果,无论它是通过还是失败 - 我们不能无限期地重试测试,在本例中,为了简单起见,我们设置了一次重试的限制。
请注意,当我们重试测试时,它必然在同一版本的代码上执行,但它可能观察到一个不同的外部状态。然而,由于两次尝试在时间上非常接近,观察到不同的外部状态的概率非常低。事实上,我们的测试基础设施有意识地努力确保对特定测试的所有重试都观察到一个非常相似的外部状态。例如,我们将所有尝试固定在同一版本的配置或外部服务上执行。正因如此,在我们的模型中,我们可以假设观察到的外部状态在两次尝试之间没有变化。
把拼图的所有部分拼在一起,我们可以根据测试的两个参数条件,计算出可能观察到的测试结果的概率:
pb-不良状态的概率
pf-良好状态下的失效概率
度量脆弱性
该模型允许我们评估观察到特定测试结果序列的概率,前提是本测试提供了这两个参数的特定值。然而,这些参数的值不是先验的。我们需要以某种方式反转模型,以便根据观察到的测试结果序列对其进行估计。
我们使用条形图表示特定测试的一系列最新结果,其中绿色或红色条形的高度分别统计在特定版本的代码和外部状态下发生的通过或失败的尝试数。
来拯救我们的是贝叶斯定理
我们在统计建模环境 Stan 中表示了该模型,Stan 实现了最先进的贝叶斯推理算法。这使我们能够有效地将最近观察到的特定测试结果序列转化为两个参数的分布。
注意,我们的模型没有生成 PFS 的点估计值,而是生成了一个完整的后验分布。这让我们能够量化我们对脆弱度评分估算的信心:
当分布很窄且集中在特定脆弱度分数周围时,我们可以相信估计值,因为脆弱度的真实程度极不可能与我们的估计值有显著差异。
当分布较宽或具有多个不同的局部极大值时,这表明我们的模型无法自信地估算特定测试的脆弱性,因此我们必须考虑用更多测试结果来评估测试的脆弱程度。
下面,我们给出了四个真实测试的示例,以及一系列最近的结果,这些结果描述了每个测试行为的两个参数的估计值。这些例子表明,由我们的统计模型测量的脆弱性与开发人员期望看到的特定测试的一系列最新结果相匹配。注意,该模型正确地捕捉到了完全确定性测试中断的情况——例如,由于一段时间内全局配置的变化——在这种情况下,尽管观察到许多故障,但测试的 PFS 接近于零。
我们如何使用 PFS?
PFS 为我们提供了测试脆弱度的测量方法,即:
通用性,就其适用于任何测试而言,
仅基于观察到的结果序列,这意味着它在没有任何定制的情况下工作,并且适用于在任何编程语言或测试框架中实现的测试,以及
指明了估算特定测试的脆弱程度的置信水平。
自 2018 年年中首次部署以来,PFS 已达到多个目的。
胡萝卜和大棒
PFS 是我们对抗不可靠测试对开发人员体验产生负面影响的第二大强大武器,仅次于耗费大量资源的重试。
基于连续集成系统正常运行期间产生的测试结果,我们持续计算并维护所有测试的最新分数值。注意,为了使用我们的统计模型计算 PFS,我们不需要额外的测试运行。相反,当开发人员对代码库进行更改并通过持续集成系统进行测试时,我们可以利用在正常情况下已经发生的结果。通过将统计模型拟合到特定测试结果的历史记录来计算分数,仅需要几分之一秒,因此可以在每次测试和每次有新结果生成时进行计算。
我们在特定的测试类仪表板上显示 PFS 的历史值,以便开发人员可以定位分数何时发生更改,并更容易地确定其根本原因。
我们不仅依赖于测试作者的善意,还依赖于激励制度,以保持我们庞大的测试套件的可靠性。当一个特定测试的脆弱性发生衰退并开始趋向更严重时,我们为该测试申明的所有者开一张罚单,无论所有者是个人还是团队。严重恶化,或未按时修复的测试被标记为不可靠,这使得它们不符合基于变更的测试类型。导致的结果是,我们的持续集成系统在发生变更时不会选择此类测试来运行,以避免潜在的系统影响。这通常是测试作者提高特定测试可靠性的强大动机,因为当其他开发人员要更改我们的巨石代码库时,测试作者在很大程度上依赖测试来强制执行与他们的契约。
通过这种方式,我们将 PFS 用作胡萝卜和大棒,既鼓励测试作者保持其测试的可靠性,也惩罚那些让测试恶化到会对在同一代码库中工作的其他工程师的生产力产生负面影响的人。PFS 帮助我们解决自然紧张关系,并在测试作者和更改代码库的开发人员之间建立社交契约。虽然前者被迫保持其测试的合理可靠性,但后者必须解决可靠测试在其代码更改中发现的任何问题。
指标是信任
随着时间的推移,随着 PFS 获得了可信度,并且作为一种反映开发人员对测试脆弱性感知的衡量标准,越来越被广泛接受,我们看到大型团队使用它来设定目标并推动测试可靠性改进工作。这是工程师对 PFS 工作表现良好的信任程度的最好证明。
PFS 是基于所有测试在某种程度上都是不可靠的假设而开发的。当用于推动组织范围内的测试质量投资时,这一点很重要,原因有很多。分数有助于识别最脆弱的测试,从而将开发人员的时间分配给那些测试活动,这些测试在改进后,将最大程度地降低观察到的脆弱性的总水平。虽然它不一定能告诉你改进测试需要多少工作,但它确实能告诉你预期的回报。
PFS 还有助于确定何时停止测试质量改进工作。由于所有测试都是脆弱的,投入人力资源来提高测试可靠性最终会导致回报递减。每个测试框架和测试环境都会带来固有的脆弱程度,这是无法通过改进测试来减少的。PFS 允许我们比较在特定测试框架中展示的真实世界测试的脆弱度,与使用相同框架实现的尽可能简单的测试的脆弱度。当这两个分数趋于一致时,这表明我们无法使测试更加可靠——除非我们决定改进框架本身。我们已经观察到,这种有效脆弱度的下限取决于所讨论的测试框架。对于单元测试,它远低于 1%,而对于一些端到端测试框架,它达到 10%。
基于测试框架本身会导致部分的测试脆弱度的观察,我们发现了另一个证据。最近在脆弱度测试社区中的一个项目,导致了一个新的内部端到端测试框架的开发,这个框架让编写出不可靠测试的概率极低,而且框架自身也几乎没有脆弱度问题。
我们要感谢以下工程师,感谢他们对项目的贡献:Vladimir Bychkovsky, Beliz Gokkaya, and Michael Samoylenko。