不懂变异测试,你好意思说自己是测试工程师,今天让我(一个即将秃头的工程师)带你深入浅出理解变异测试的方方面面。 从测试覆盖率的局限性谈起很多时候我们会用单元测试执行后的代码覆盖率来衡量测试的充分性和完整性,问题是有了很多测试用例,同时又有很高的白盒覆盖率,是否代码质量真的就高枕无忧了吗? 答案显然不是。来看个《软件研发效能提升之美》书里的例子就懂了。 比如下面的代码,测试执行后的代码覆盖率可以达到100%,但是代码中的问题并没有能暴露出来,这样的测试用例缺乏对于缺陷的发现能力,会导致测试用例数量不少,同时测试覆盖率也很高,但是缺陷依旧不能被发现的尴尬处境。 图1:覆盖率不等于有效性的例子 这里暴露出的本质问题是测试覆盖率不等于测试的有效性,那么测试的有效性又应该如何来衡量呢?这就是变异测试(Mutation Testing)需要解决的问题和存在的价值。 变异测试的基本概念首先解释一下变异测试的概念。变异测试是一种基于错误注入的测试方式,具体来讲就是人为在代码中注入错误,然后来观察现有的测试用例是否能够发现这些错误,如果能够发现说明测试用例是有效的,如果不能发现说明测试用例需要进一步完善和补充。 你是不是有一种亲切感,这个不就是混沌工程(Chaos Engineering)的逻辑吗?没错,变异测试本质上就是代码级的混沌工程。 变异测试是新技术吗?变异测试并不是什么新技术,变异测试的概念早在1971年由Richard Lipton提出,之后在1980年就已经出现了第一个变异测试的工具。这个历史远比混沌工程要早得多。 在学术界变异测试的研究已经持续了很长时间,研究的焦点主要集中在变异算法优化以及等价变异体分析上。 但是在工业界对变异测试的关注度一直很低,甚至很多测试技术人员压根不知道什么是变异测试,这背后的原因主要是因为变异测试需要在单元测试已经做得比较完备的基础上才有其价值,但是国内的单元测试现状不用我说,你也懂的,这也就制约了变异测试在工业界的实践。 实施变异测试的步骤实施变异测试的简化步骤如下图所示。 图2:简化后的变异测试步骤 首选我们有被测源代码,以及对应的测试用例代码,随后在被测源代码P上,用变异算子S生成变异体源代码P’,这个过程称为变异体生成。用人话来讲其实就是对被测源代码进行“合乎语法的微小改动”,这种微小改动就是所谓的变异算子,比如原本是“加法”运算现在改成“乘法”运算,或者原本是“逻辑与”运算现在改成“逻辑或”运算,之后分别使用相同的测试用例T对被测源代码P和变异体源代码P’执行测试,最后比较测试执行结果。 如果这么解释还是比较抽象的话,我这里使用变异测试工具MutPy基于Python语言来举个例子。 被测源代码是一个简单的乘法函数(图3),测试用例代码对该乘法函数的正确性进行了验证(图4),可见,测试代码执行后的代码覆盖率是100%。 图3:被测源代码 图4:测试代码 接下来使用MutPy发起变异测试,具体做法是在calculator.py和test_calculator.py的目录下执行以下命令行。 变异测试执行后的结果输出如图5所示,其中运用了4个变异算子,其中3个变异被杀死了,1个变异存活了下来。 4个变异算子分别实现了把乘法换成除法、取余、幂和直接返回。可以看到其中幂变异存活了下来,而其他3个变异都被杀死了,因此最后的变异得分是75%。这说明这个测试用例对于幂变异是无法发现的,所以我们需要对测试用例进行修正,这里最简单的方式就是把测试用例中的22=4换成23=6即可,这样再执行一遍变异测试就能杀死所有的变异体。 图5:MutPy变异测试执行结果 有了主观感受之后,我们再来看一下变异算子S的定义。 变异算子是在符合语法规则的前提下,将原有代码转变成极小差别代码(变异体)的转换规则,这些转换规则有一整套的定义,图6列出了这些常用变异算子的定义和缩写,上面的例子中就用到算数运算符替换AOR和语句删除SDL。 图6:常用变异算子的定义 主流变异测试工具使用简介了解了变异测试的基本概念后,这里再给大家介绍一些目前比较主流的变异测试工具。 变异测试工具其实不少,但是很多都是学术界的产物,很难在工业界实际落地与应用。我这里挑选三款在工业界有实际应用价值的工具给大家做个简单的介绍。 PitestPitest是针对Java目前主流的变异测试工具,其功能比较强大,属于可以应用于“真实世界”的变异测试工具。 Pitest不仅使用简单,执行性能优越,而且还提出了变异覆盖率的指标,变异覆盖率指标可以和代码覆盖率指标同时使用。 比如下面图7中的Pitest测试报告,其中: 浅绿色的代码行表示已经被测试用例覆盖(比如125行和126行); 深绿色的代码行表示已经被变异测试覆盖(比如123行和123行,最左边的3代表这一行已经覆盖了3种变异); 浅粉红色的代码行表示没有被测试用例覆盖(比如130行); 深粉红色的代码行表示还没有被变异测试覆盖(比如127行和128行,这两行最左边的1代表这一行需要覆盖1种变异);
图7:Pitest测试报告中的覆盖率 由于Pitest比较实用,我这里给大家举一个完整的实际案例来帮助大家更好地理解。 首先,图8是被测代码,图9是测试用例代码。 图8:被测代码
图9:测试用例代码 可以发现图9中的单元测试有很明显的问题,作为测试居然没有任何断言Assert,同时最后一个单元测试的语义也是错的,这样的测试用例有效性是完全不符合要求的。但是这种明显不符合要求的单元测试覆盖率却可以达到100%,如图10所示。这里再次印证了覆盖率不等于有效性。 图10:单元测试覆盖率结果100% 此时如果用Pitest去执行变异测试,得到的结果报告就能反映出问题了,如图11所示。
图11:Pitest的覆盖率报告能反映问题 从Pitest的报告中可以看出来,虽然代码覆盖率是100%,但是变异覆盖率却是0%,为此我们需要对测试用例进行改进,改进后的测试用例如图12所示,我们增加了测试用例的断言Assert,同时修正了最后一个测试用例的语义错误。 图12:改进后的测试用例 之后再次用Pitest去执行变异测试,得到的结果报告如图13所示。可见此时变异覆盖率达到了60%,结合具体的覆盖率详情(图14),我们发现第9行代码的5种变异中,有2个边界值条件存活了,这说明当前的测试用例并没有考虑边界值的场景。
|