突变驱动测试:当TDD不够好时

2021-02-06 20:20:04

作为一个喜欢讨论软件工艺和最佳实践的人,测试驱动开发(TDD)对我来说有点痛。首先,我要说我喜欢TDD对测试的重视。太多的软件项目无法进行测试。结果是不言而喻的,因为要实施变更需要成倍的时间,而且人们甚至都不敢碰任何东西,结果要说明很多年。

话虽如此,我仍然从不热衷于TDD。一方面,它太严格了。首先坚持编写测试,通常会妨碍探索工作–在确定正确的接口,方法和OO结构应该是什么之前,需要做的工作。

但是,另一方面,TDD太宽容了。许多从业者认为,由于他们正在练习TDD,因此他们的测试套件坚如磐石。实际上,我看到在TDD期间编写了太多测试,但仍然面临覆盖漏洞。覆盖漏洞,无论现在还是将来,都可能会导致生产错误。就测试方法论而言,填补这些漏洞是您的头等大事-取代所有其他时尚和建议。

因此,为什么我最喜欢的测试原理可以通过以下步骤顺序的突变驱动测试得到最好的说明:

进入既拥有代码又成功通过测试的状态,应该在其他地方辩论是编写代码优先还是测试优先。当然,欢迎使用TDD达到这一点

逐行浏览新添加/修改的代码,并手动注入一个错误在确定什么是“合理的错误”时,请假定粗心,懒惰,缺乏经验和能力不足。但不是恶意。使用测试来捕获恶意错误会成倍增加难度,并且不太现实

如果测试没有失败,请查看测试中存在哪些覆盖漏洞,并通过添加新测试或更新现有测试来修复它们

返回第2步并重复,直到您注入了您可以想到的每个错误,或者用完了所有时间。当您对Mutation-Driven-Testing有所了解时,您将找到一个直观的诀窍来找出哪些错误最有可能超越您的平均测试套件。这将大大加快此过程,并教会您编写更全面的测试

突变驱动测试背后的原理很简单。评估测试套件可靠性的唯一方法是查看存在错误时是否失败。因此,请自己注入该错误。使用结果输出找出覆盖漏洞的位置,并根据需要进行增强。不仅要捕捉您刚刚注入的特定错误,而且还要捕捉其他类似类别的错误。这样,您可以确定测试套件中的盲点,并相应地增强测试范围。

如果您仔细听讲,您会听到一百万个TDD支持者大声抗议。

“但是,如果您确实完成了TDD,则无需进行突变测试!如果您正确执行TDD,则每一项功能都会进行专门的测试,因此您永远不会遇到漏洞!”

为了说明为什么这是不正确的,并且为了说明突变驱动测试的好处,我整理了以下示例。当您搜索“ TDD示例”时,示例的来源是Google排名最高的结果。

有人可能会认为本文的作者不是TDD的好榜样。 “没有真正的TDD实践者”会像上面的示例一样编写测试。但这真的只是酸葡萄。作者正在共同努力编写综合测试,做得很好,下面选择的示例非常简单。 TDD在工作场所的现实是,大多数从业人员都不是完美的,并且总是容易受到疏忽。可以使用Mutation-Driven-Testing标记并修复的疏忽。

事不宜迟,让我们深入研究示例-创建一个简单的基于字符串的计算器。为了简洁起见,让我们仅看一下示例中的前三个要求以及它们的实现和测试。

当然,这似乎是涵盖所有功能的一组不错的测试。但是,它经受突变驱动测试的效果如何?在现实世界中,我会一次注入一个bug,并在每次注入之后运行测试。但是为了简洁起见,让我们一次注入所有相关的错误。

这有点免费,但值得指出。如果我们只是从实现中删除trim()调用,该怎么办。当然,这似乎是一个合理的监督。

要求说为空字符串返回0。据推测,基于作者的实现,这意味着任何空子字符串应被视为0,而先前的非空子字符串仍应加和。但是,如果实现做一些不同的事情并在看到任何空字符串后立即返回0,该怎么办?

要求说该方法可以采用“ 0、1或2个数字”。所以…。我们应该检查3个数字并抛出异常?

诚然,这是一个非常愚蠢的错误,但永远不要低估创意傻瓜的能力。

构建“字符串->”时数字”计算器,其最终结果为整数,有许多不同的方式来实现字符串转换:

将字符串转换为int,执行int操作,返回int结果。如果您的输入字符串不是整数,则引发异常

将字符串转换为double,将double转换为int,执行int操作,返回int结果

将字符串转换为double,执行double操作,将最终结果转换为int并将其返回

当给定输入" 1.5,1.5"时,上述3种方法都会产生完全不同的输出。在示例中,作者实现了选项#1。让我们假设这确实是所需的行为。但是,如果他错误地实施了方案3,该怎么办?

实际上,我们一次只能注入一个bug。但是为了简洁起见,让我们将所有这些结合起来,使我们陷入以下混乱:

令人惊讶的是,没有一个测试失败!尽管我们在其他各行中注入了可能存在的错误,但作者编写的每个测试仍然是绿色的。证明我们的测试套件存在以下漏洞:

一旦确定了上述漏洞,就可以通过添加更多测试来开始堵塞它们。现在测试失败了,一旦恢复所有注入的错误,测试就开始通过:

让我明确地说-上述测试当然不是完美的。随着突变测试的进一步迭代,您可以发现上述测试遗漏的更多覆盖孔。另外,您确实应该使用更为复杂的测试技术(在此讨论),以便以简洁的方式增强测试覆盖率。

但是至少这个过程可以帮助我们更好地了解覆盖漏洞的位置,并使我们更加接近消除最明显的漏洞。

诚然,完成上述过程将花费更多的精力和时间。最终结果将是一套更为冗长的测试,其中许多对于未经训练的人来说似乎是多余的。这真的值得吗?

与往常一样,这取决于您的优先级。如果您要构建快速且肮脏的原型,并且不介意漏出较小的极端情况,则可能很好。但是,如果生产错误使您感到恐惧,则绝对应该花费时间和精力来增强测试套件。具有最小覆盖漏洞的坚如磐石的测试套件是抵御生产错误的最佳选择。从长远来看,通过允许人们安全地重构并快速部署更改,而无需花费大量时间进行手动测试,它实际上将提高您的开发速度。

人们经常谈论TDD,好像它是“解决”测试的灵丹妙药。显然,事实并非如此。也许,如果您是Jeff Dean或Sanjay Ghemawat,则可以纯粹通过推理来编写完美的测试套件。但是对于我们其余的凡人而言,识别和修复测试套件中覆盖漏洞的最佳方法是凭经验进行测试。