笔者团队在比较了各种方案后,选择用变异测试(MutationTesting)来度量测试用例的有效性。
1.变异测试的原理
变异测试就是向应用代码中注入一个Bug(我们把此处的Bug叫作变异),看看测试代码能否发现这个变异,以此来验证测试代码发现Bug的能力。
例如,我们向应用代码中注入变异,把b<100改为b-100。然后我们针对变异后的应用代码执行一组测试用例。如果其中有一个或多个测试用例对之前的应用代码是通过的,但对变异后的应用代码是不通过的,我们就认为这组测试用例发现了这个Bug,这组测试用例对这个Bug是有效的、有发现能力的,如图1-4所示。
▲图1-4注入变异测试的过程
基于变异测试,我们对测试用例有效性的定义是:有效性=发现的变异数量/注入的变异总数。
从原理上讲,代码变异主要分为以下三类。
第一类可以称为等效变异(EquivalentMutation)。例如,把a+b变成a?(?b)。这种变异我们不做,因为它不是一个Bug。
第二类可以称为实际等效变异(PracticallyEquivalentMutation)。例如,把int类型变成short类型,理论上会有溢出的问题。一般情况下大家很少用short类型,只要是整数就直接用int类型,但值的范围无论如何都不会超过几十或几百。比如,变量numberOfDays的值是天数(NumberofDays),这种变异我们也不做。
剩下的那些变异,比如算术运算符替换(ArithmeticOperatorReplacement,AOR)和条件运算符替换(ConditionalOperatorReplacement,COR),几乎都是Bug,很清楚、没争议。我们主要做的就是这类变异。
在实操过程中,常用的变异类型如表1-1所示。
▼表1-1常用的变异类型
2.工程化
我们基于已有的持续集成基础设施,实现了规模化的变异测试,如图1-5所示。
对每个服务的应用代码都自动生成大量不同的变异。
对每个变异,都将变异注入应用代码中,结合精准测试的用例筛选执行被度量的测试用例集,根据执行结果判断是否发现了该变异。
汇总数据,得到每个服务的测试用例集的变异发现率(测试用例有效性)。
每隔一段时间就重复上面这个过程,进行新一轮的度量。
合理安排CI调度,见缝插针地利用测试资源使用率较低的时段,尽可能地缩短每轮度量之间的时间间隔。
▲图1-5规模化的变异测试
这套工程化的测试用例有效性度量方案的特点如下。
接入门槛低:只需给出应用代码和测试代码的Git仓库地址,即可进行评估,得到改进报告。
普遍适用:无论是什么语言开发的应用代码,无论测试用例是用什么测试框架编写和执行的,都可以接入。
评估准确:打通了代码覆盖率数据,变异只会注入已经被测试用例覆盖了的代码行,减少了无效注入。
另外,使用这套基于变异测试的用例有效性度量方法,不要求用例本身具有很高的稳定性。这是因为:如果测试用例集是稳定的,不稳定的测试用例(FlakyTests)很少,那么对每个变异可以只执行一遍该用例集。如果用例集本身是不稳定的,其中一些用例本身就是时而通过、时而失败的,那么可以对该用例集执行多次,每个用例只要在多次中通过一次就算通过,只有全部失败的才算失败,这样就可以排除用例不稳定的影响,获得比较准确的用例有效性度量结果。
3.效果
▼表1-2基于变异测试的有效性度量的部分实际度量结果
从实际结果得到的数据是:一个服务的单元测试和接口测试的有效性如果可以达到90%左右,那么用例的质量是比较好的,用例写了却没有发现Bug的情况也会比较少。下一步的计划是对功能回归用例集进行这样的定期度量。目前的主要问题是执行功能回归用例集的硬件成本比较高,而基于变异测试的有效性度量方案需要成百上千次地执行被度量的用例集。
4.进一步优化
上述的测试用例有效性度量方案在实际运用中还遇到了以下两个难题。
性能:对于几千个应用,如何低成本地实现更短的反馈?
“杀虫剂效应”:变异的类型如何不断更新,防止出现“杀虫剂效应”?
(1)从注入策略上提升性能。在最初的工程化实践中,我们的模式是将一个变异注入分支,并运行被评估的用例集。如果被测系统的变异点是1000个,那么意味着对一个系统完成一次评估需要执行1000次被评估的用例集。这个成本是非常高的。
对此,我们在注入策略上进行了以下升级,如图1-6所示。
多个变异在同一个代码调用路径下,不能同时注入。
多个变异在不同的代码调用路径下,可以同时注入。
一次执行一次变异和一次执行多次变异进行A/B测试,保障算法的正确性。
▲图1-6注入策略的升级
经过这样的优化,评估轮次平均降低了70%左右。
(2)杀虫剂效应。软件测试中的“杀虫剂效应”(PesticideParadox)指:如果不断重复相同的测试手段,那么软件会对测试“免疫”,一段时间以后,该测试手段将不再能发现新的Bug。这是因为多次使用后,在这些测试手段关注的地方和问题类型中,Bug已经被修复得差不多了,而且在这些地方,程序员也会格外关注和小心。最终软件对这些测试“免疫”。
变异测试也会产生“杀虫剂效应”。例如,如果我们一直进行“>变成>=”的注入,那么程序员就会对>和>=的边界条件特别关注。为了避免在变异测试中出现“杀虫剂效应”,要持续地更新变异策略。对此,我们采取的方法是不断地从代码历史中“学习”更多的Bug类型,并在变异测试中复现这些Bug类型。1.8节会介绍我们是如何从代码历史中“学习”的。