随着工程师为我们的应用开发新功能和优化,Facebook的代码库每天都在变化。如果未通过验证,则这些更改中的每一个都可能使全球数十亿人的产品功能或可靠性下降。为了减轻这种风险,我们维护了大量的自动化回归测试套件,以涵盖各种产品和应用程序。这些测试在开发过程的每个阶段运行,可帮助工程师及早发现回归并防止其影响我们的产品。
尽管我们使用自动化测试来检测产品质量的下降,但是直到最近,我们还没有办法自动检测测试本身是否在恶化。自动化测试是另一种软件,随着代码库的发展,该软件可能会随着时间的推移变得不可靠。不可靠的测试(也称为易碎测试)会产生错误或不确定的信号,从而破坏工程师的信任,从而破坏整个回归测试过程的有效性。如果测试有时通过并且有时失败,而没有更改基础产品或应用程序,则它将迫使工程师花费时间去寻找甚至不存在的问题。这破坏了他们对测试过程的信任。因此,确定测试是否易碎很重要。
迄今为止,学术研究主要集中在试图确定哪些测试是片状的和哪些是可靠的。但是,软件工程实践表明,即使按照最佳工程原理进行实施,所有真实世界的测试在某种程度上也存在问题。因此,要问的正确问题不是特定测试是否为片状,而是它有多片状。为了回答这个问题,我们开发了一种测试脆弱性的方法:概率脆弱性评分(PFS)。使用此度量,我们现在可以测试测试以测量和监视其可靠性,从而能够对测试套件质量的任何下降做出快速反应。
通过PFS,我们可以量化Facebook上每个测试的易碎程度,并监控其可靠性随时间的变化。如果我们发现特定的测试在创建后不久就变得不可靠,则可以引导工程师将注意力集中在修复它们上。该分数是通用的,这意味着它适用于所有回归测试,而与所使用的编程语言或测试框架无关。此外,它可以被有效地计算,这使我们能够对其进行扩展以实时监控数百万个测试的可靠性。普通工程师可以解释和理解这种测试剥落性的度量。因此,许多Facebook团队已采用PFS来设置测试可靠性目标,并推动跨团队的工作来修复片状测试。
当测试不稳定时,工程师会迅速学会忽略它并最终将其删除,这增加了将来代码更改降低功能或质量的风险。几年前,我们创建了一种使用机器学习来预测针对特定代码更改运行哪些测试的方法。当时,我们意识到小型的针对性测试可能有些不稳定,但是通过重试或修改测试是可以容忍且容易解决的。
但是,当我们进行更大的端到端测试时,可靠性是一个更大的挑战。多次重试非常耗时,而尝试修改测试则要复杂得多。进行代码更改时,我们需要一个非常可靠的信号,因此开发人员不会浪费时间去寻找根本不存在的问题。当时,我们假设真正可靠的测试不会在没有测试的情况下显示出回归。要知道哪些测试真正可靠,我们需要一种自动化的方法来测试。
我们着手寻找不会表现出任何不可靠行为(有时通过,有时没有通过)的测试。我们很快意识到没有任何东西-所有的端到端测试都有一定程度的脆弱性。总是有可能出问题的地方,这会影响测试的可靠性。根据多种因素,昨天可靠的测试今天可能会变得不稳定。这意味着我们还需要持续监控测试的可靠性,以便在测试的脆弱性超出允许范围时发出警报。
最终,我们使用PFS的目标不是要断言任何测试都是100%可靠的,因为那是不现实的。我们的目标是简单地断言某个测试足够可靠,并提供一个量表来说明哪些测试的可靠性不如应有。
在没有任何其他信息的情况下,我们必须以单项测试的执行结果为准-没有依据来判断该结果是测试脆弱性的症状还是测试已检测到回归的合法指标。但是,如果我们多次执行特定的测试并且显示出恒定的脆弱性,则可以合理地预期观察到结果的具体分布。
为了测量任何测试的脆弱性,我们开发了一种统计模型,该模型产生的结果分布类似于如果测试多次表明存在已知的脆弱性,则该结果将被观察到。我们已经用概率编程语言Stan实现了该模型。
如果我们设置了假设测试的脆弱性水平,该模型可以让我们生成测试结果的分布。但是,在使用该模型时,我们知道具体的实际测试的最新测试结果,并希望估计该测试所表现出的脆弱性。允许我们反转模型的过程称为贝叶斯推理。概率编程语言运行时可以非常有效地实现它,因此我们不必为此担心。
为了更好地理解我们的统计(定量)模型,我们将首先考虑一个更简单,定性的模型。简化的模型隔离了影响测试结果的三个主要因素:
测试是否失败肯定取决于被测代码和定义测试本身的代码。希望测试结果依赖于代码版本。测试通常有可能会包含无效的断言,尽管通常会快速识别并修复或删除此类测试。
许多全面的,更端到端的测试也依赖于生产中部署的服务才能正常运行。其中一些甚至检查受测代码与那些服务的兼容性。正如被测代码的行为取决于功能选通和各种配置一样,行使此代码的测试结果也是如此。因此,测试结果对世界状况的依赖性通常是不可避免的,有时是可取的。
最后,我们有一个垃圾桶。影响测试结果的其他任何因素都属于此类别。特别是,有许多不确定的因素会影响测试的执行,例如竞态条件,随机性的使用或通过网络通话时的虚假故障。我们称这种垃圾桶为垃圾,这正是我们要测量和理解的。
在上述定性模型中,测试结果相对于最后一个因素的敏感程度构成了我们对薄片性的衡量。从概念上讲,如果测试的结果是可区分的,则我们可以如下记下脆弱性得分。
不幸的是,测试的结果是二进制的,我们无法计算上述偏导数。我们必须找到另一种方法来量化特定测试结果相对于片状因子的敏感性。
在我们构建统计模型的早期尝试中,我们偶然发现了一个有趣的问题:基础数学关于替换合格和不合格的测试结果是对称的。根据我们自己的软件工程经验,我们知道这现在反映了开发人员如何看待测试脆弱性。我们进行了以下实证观察:
通过的测试表明没有相应的回归,而失败只是提示再次运行测试。
在如何处理合格和不合格的测试结果方面,这种不对称性是软件开发的特质。尽管工程师倾向于相信通过测试的结果,但他们经常在相同版本的代码上重试失败的测试多次,并认为失败后再通过测试结果是不稳定的。对于这种行为为何普遍存在,我们没有很好的理论解释。
结合以上观察结果,我们可以如下编写测试性脆性的统计量度,即概率性脆性得分。
直观地,该分数衡量了测试失败的可能性,前提是该测试可以通过相同版本的代码并在相同的世界环境下通过,并且可以任意多次尝试。根据我们的经验观察,如果测试可以在任意次重试之后通过,则在相同版本的代码和世界状况下观察到的任何失败都必须视为不稳定。
在上面的公式中,我们使用条件概率,只要满足特定条件(任何观察到的失败都是片状的),就可以捕获发生特定事件(测试失败)的概率的概念。
为了设计我们的统计模型,我们假设PFS是每个测试的固有属性,换句话说,每个测试都有一个类似的数字,而我们的目标是根据观察到的测试结果顺序来估计它。在我们的模型中,每个测试的参数不是一个,而是两个这样的数字:
错误状态的概率,它衡量由于被测代码的版本或整个世界而导致测试失败的频率。
该模型无法让我们预测特定测试的未来结果,也无法告诉我们特定失败是由代码版本,世界状况还是脆弱引起的。但是,它确实让我们评估了以特定脆度为特征的测试产生给定测试结果序列的可能性。
我们可以在一个非常简单的示例上看到该模型的实际作用。考虑一个特定的测试,该测试已在代码c的版本上并且在世界w 1的状态下运行。如果第一次尝试通过,那么根据观察到的测试结果不对称性,我们就完成了;我们不会重试通过测试执行。但是,如果第一次尝试失败,我们将重试一次。我们将第二次尝试视为测试的最终结果,而不管测试是通过还是失败-我们无法无限期地重试测试,在此示例中,为简单起见,我们设置了一次重试的限制。
请注意,当我们重试测试时,必须在相同版本的代码上进行测试,但是它可能会观察到不同的状态。但是,由于两次尝试的时间间隔都非常接近,因此极不可能观察到世界的不同状态。实际上,我们的测试基础架构做出了有意识的努力,以确保特定测试的所有重试都观察到非常相似的世界状况。例如,我们将所有尝试锁定到相同版本的配置或外部服务。因此,在我们的模型中,我们假设观察到的世界状态在两次尝试之间不会改变。
综合所有难题,我们可以根据测试的两个参数来计算出观察可能的测试结果的可能性:
该模型允许我们评估观察特定结果的序列的可能性,前提是所讨论的测试具有两个参数的特定值。但是,这些参数的值不是先验的。我们需要以某种方式反转模型,以便根据观察到的测试结果顺序对其进行估算。
我们已经在统计建模环境Stan中表达了该模型,该模型实现了最新的贝叶斯推理算法。这使我们能够有效地将针对特定测试的一系列最近观察到的测试结果转换为两个参数的分布。
请注意,我们的模型不是产生PFS的点估计,而是产生整个后验分布。这使我们可以量化我们对脆弱性得分的估计有多自信:
当分布范围狭窄且集中在特定的脆度得分附近时,我们就可以相信该估计值,因为真正的脆度程度与我们的估计相差不大。
当分布很宽或有多个不同的局部最大值时,这表明我们的模型无法自信地估计特定测试的脆弱性,因此我们必须考虑更多测试结果以评估测试的脆弱性。
下面,我们提供了四个实际测试示例,以及一系列最新结果和描述每个测试行为的两个参数的估计值。这些示例表明,根据我们的统计模型测得的脆弱性符合开发人员的预期,即盯着特定测试的一系列近期结果。请注意,该模型正确地捕获了确定性测试中断的情况(例如,由于一段时间内全局配置的更改而导致的中断),在这种情况下,尽管观察到许多失败,但是测试的PFS接近于零。
仅基于观察到的结果序列,这意味着它无需任何自定义即可工作,并且适用于以任何编程语言或测试框架实施的测试,并且
表示人们对特定测试的脆弱程度有多自信。
PFS是我们第二强大的工具,可抵御不可靠的测试对开发人员的体验产生负面影响,仅次于资源密集的重试。
根据持续集成系统正常运行期间产生的测试结果,我们会持续计算并维护所有测试的最新分数值。请注意,为了使用我们的统计模型计算PFS,我们不需要进行其他测试。相反,当开发人员将更改提交到代码库并由持续集成系统对其进行测试时,我们可以扛起通常情况下已经发生的事情。通过使统计模型适合特定测试结果的历史记录来计算分数需要花费一秒钟的时间,因此可以针对每个测试以及每次生成新结果进行此操作。
我们在特定于测试的仪表板上显示PFS的历史值,以便开发人员可以找到分数更改发生的时间,并更容易确定其根本原因。
为了使我们庞大的测试套件可靠,我们不仅依赖测试作者的信誉,而且还依赖激励机制。当特定测试的脆弱性下降并开始趋于上升时,我们将为已声明的测试所有者(无论是个人还是团队)创建票证。严重恶化和/或未按时修复的测试被标记为片状,这使它们不适合进行基于更改的测试。因此,我们的持续集成系统不会选择此类测试来运行可能影响它们的更改。对于测试作者来说,这通常是足够强大的动力,可以提高特定测试的可靠性,因为他们严重依赖于测试来与更改我们整体代码库的其他开发人员执行合同。
这样,我们将PFS当作一根胡萝卜和一根棍子使用,既鼓励测试作者保持测试的可靠性,也要惩罚那些使测试质量下降到对其他从事此工作的工程师的生产率产生负面影响的程度。相同的代码库。 PFS帮助我们解决自然紧张的问题,并在测试作者和更改代码库的开发人员之间建立了社会契约。尽管前者被迫保持其测试相当可靠,但后者必须解决可靠测试在其代码更改中发现的任何问题。
随着时间的流逝,随着PFS赢得信誉并被更广泛地接受,以反映开发人员对测试脆弱性的理解,我们看到大型团队使用它来设定目标并推动测试可靠性改进工作。这是对PFS做好工作的信任工程师水平的最好证明。
PFS是基于所有测试在一定程度上呈片状的假设而开发的。当用于驱动组织范围内的测试质量投资时,出于多种原因,这一点很重要。该分数有助于识别大多数片状测试,从而将开发人员的时间分配给那些测试,这些测试在得到改进后,将最有可能降低所观察到的片状感的总水平。尽管它不一定告诉您改进测试需要多少工作,但它确实告诉您期望的回报。
PFS还有助于确定何时应该停止测试质量改进工作。由于所有测试都不可靠,因此投入人力资源提高测试的可靠性最终会导致收益递减。每个测试框架和测试环境都会带来固有的脆弱性,而这种脆弱性无法通过改进测试来降低。通过PFS,我们可以将在特定测试框架中表达的真实测试的脆弱性与使用同一框架实现的最简单测试的脆弱性进行比较。当这两个分数趋于一致时,这表明人们无法使测试更加可靠-除非人们决定改进框架本身。我们已经观察到,根据不同的测试框架,这种有效的脆性下限会有所不同。对于单元测试,它远远低于1%,而对于某些端到端测试框架,它达到10%。
此后,有关测试框架本身对部分测试脆弱性的观察得到了另一个确认。在测试脆弱性领域中的一个最新项目导致开发了一个新的内部端到端测试框架,这使得编写不可靠的测试变得极为困难,并且几乎没有造成任何脆弱性。
我们要感谢以下工程师,并感谢他们为该项目做出的贡献:弗拉基米尔·别奇科夫斯基,贝利兹·高卡亚和迈克尔·萨莫连科。