以注入缺陷的方式查找缺陷:精妙的变异测试
在软件开发中,当一个项目的开发周期接近尾声,所有新功能都已完成且所有测试都通过时,作为测试经理,你是否就可以决定发布软件了呢?实际上,即便所有测试都通过,也不能掉以轻心,因为测试套件可能并不够好。那么,如何检验测试套件发现缺陷的有效性呢?变异测试提供了一种简单、优雅且有效的方法。
评估测试套件质量
工程师和管理者喜欢量化事物,测试套件的质量也不例外。为了衡量测试套件的质量,人们开发并应用了各种覆盖指标,常见的有代码覆盖、分支覆盖和条件覆盖。
-
代码覆盖
:检查测试套件中是否执行了各个语句。若测试未执行某语句,而该语句存在触发失败的缺陷,那么这个缺陷在测试阶段就无法被发现。
-
分支覆盖
:确保每个分支至少被执行一次。
-
条件覆盖
:保证每个(子)条件分别被评估为真和假一次。
然而,这些测试质量指标存在一些问题:
1.
缺陷分布不均
:程序中缺陷并非均匀分布,存在“20%的模块包含80%的缺陷”的帕累托效应。测试套件应聚焦于易出现缺陷的模块,而非在无缺陷的类上追求高覆盖率。
2.
风险分布不均
:项目中有些模块的缺陷会带来严重后果,测试工作应根据风险分配,而非单纯追求特定的覆盖率。
3.
指标未涉及测试本身
:覆盖指标只能反映测试执行情况,不能体现测试本身的优劣。例如以下JUnit测试:
import static org.junit.Assert.*;
void testFileWrite()
{
BufferedWriter out = new BufferedWriter(new FileWriter("outfilename"));
out.write("aString");
out.close();
}
该测试只是创建文件、写入文本并关闭文件,若未崩溃就认为测试通过,但它未检查结果,可能存在创建错误文件、写入随机输出或无操作等问题。即便该测试能达到100%的任何测试标准覆盖率,也可能无法捕获最简单的缺陷。
监督测试者
检验质量保证有效性的常见方法是模拟触发警报的情况。1971年,Richard Lipton将这一模拟概念应用于测试,提出向被测软件中注入人工缺陷(即变异),并检查测试套件是否能发现这些变异。如果测试套件未能检测到变异,那么它很可能也会错过真正的缺陷,需要进行改进。
以之前的文件I/O单元测试为例,若将
BuffereredWriter.write()
方法的实现修改为无操作,原测试无法检测到该变异,说明它会错过真正的缺陷。改进后的测试如下:
import static org.junit.Assert.*;
void testFileWrite()
{
BufferedWriter out = new BufferedWriter(new FileWriter("outfilename"));
out.write("aString");
out.close();
BufferedReader in = new BufferedReader(new FileReader("outfilename"));
String contents = in.readLine();
in.close();
assertEquals("aString", contents);
}
变异测试的基本思想是向程序中注入大量人工缺陷,逐个进行测试,关注未被检测到的变异,并系统地改进测试套件,直至能发现所有变异。这种方法具有以下优点:
1. 能真正评估测试的质量,而非仅衡量测试执行的特征。
2. 高风险模块在变异时会体现出缺陷的严重后果,若测试套件未检测到这些后果,则需要改进。
3. 变异越接近真实缺陷,就越有可能复制程序中的缺陷分布。
变异测试作为评估测试套件质量的标准,已被证明优于现有的几乎所有单一指标,像Mothra(用于FORTRAN程序)和µJava(用于Java程序)等变异测试框架都可供公众使用。
高效的变异测试
变异测试存在的一个明显问题是耗时较长。每次变异后,都需要重新构建和测试程序,若变异数量达到数千个,就需要进行数千次的构建和测试。因此,变异测试需要全自动且快速的测试。以下是一些提高效率的技术:
1.
直接操作二进制代码
:对二进制代码进行变异,可避免变异后昂贵的重新构建过程,但二进制代码分析难度较大,尤其是对于复杂的变异操作符。
2.
使用变异体模式
:传统的变异测试框架会为每个变异生成一个新的变异程序版本,也可以创建一个单一版本,其中各个变异体由运行时条件控制。
3.
忽略未覆盖的代码
:只有当变异体实际被执行时,才会影响程序行为。因此,只需对测试套件覆盖的语句进行变异,并仅运行涉及该变异的测试。这种覆盖信息可通过一次测试套件运行轻松获得。
选择变异操作符
变异程序的方式有无数种,如用常量替换常量、用常量替换变量、用变量替换数组引用、用其他操作符替换操作符、用其他调用(或其他接收者)替换方法调用、用否定条件替换条件等。不过,研究表明,一小部分变异操作符的效果与大量操作符几乎相同:
1.
替换数值常量
:将数值常量X替换为X + 1、X - 1或0,指针也可设置为null。
2.
否定跳转条件
:将分支条件C替换为其否定¬C。
3.
替换算术操作符
:用其他算术操作符替换现有操作符,如用 - 替换 +、用 / 替换
、用 >> 替换 << 等。
4.
省略方法调用
*:将方法调用f()替换为常量0(若方法无返回值,则完全移除)。
这些简单的变异操作易于应用于二进制代码,且产生的变异数量较少,即所谓的选择性变异。
一个AspectJ示例
在实验中,将这些变异操作符应用于中等规模的程序,如AspectJ核心(一个包含100,000行代码的Java包,有321个单元测试,单次运行需21秒),得到了47,000个变异。通过上述优化,这些变异在14个CPU小时内完成评估。
AspectJ测试套件仅检测到53%的执行变异。例如,AspectJ中AjProblemReporter类的
staticAndInstanceConflict()
方法用于决定是否报告错误并将其报告给超类。若将对
super.staticAndInstanceConflict()
的调用抑制,错误报告也会被抑制,但测试套件无法检测到该变异。这表明测试套件虽能检查编译器是否检测到错误输入,但未检查错误是否被正确报告。
public void staticAndInstanceConflict(MethodBinding currentMethod,
MethodBinding inheritedMethod) {
if (currentMethod instanceof InterTypeMethodBinding)
return;
if (inheritedMethod instanceof InterTypeMethodBinding)
return;
super.staticAndInstanceConflict(currentMethod,
inheritedMethod); // Mutation: suppress this method call
}
等效变异体
当发现测试套件未检测到的变异时,需要添加相应的测试。但有时无法编写这样的测试,因为变异并未改变程序行为,这种变异体称为等效变异体。例如:
public int compareTo(Object other) {
if (!(other instanceof BcelAdvice))
return 0;
BcelAdvice o = (BcelAdvice)other;
if (kind.getPrecedence() != o.kind.getPrecedence()) {
if (kind.getPrecedence() > o.kind.getPrecedence())
return +1; // Mutation: replace +1 with +2
else
return -1;
}
// More comparisons...
} ...
在Java中,
compareTo()
方法比较方法目标与另一个对象,调用者仅检查比较结果的符号。将返回值从 +1 改为 +2,
compareTo()
方法和程序的语义不变。
等效变异体并不罕见,在Jaxen XPath引擎的实验中,未检测到的变异体样本中多达40%是等效变异体,它们对改进测试套件没有帮助。评估变异体的等效性通常是一个无法判定的问题,需要手动进行,且耗时较长。随着测试套件的改进,等效变异体的比例会增加。
关注影响
实验中发现大量未检测到的变异,其中相当一部分是等效变异体。我们应关注最有价值的变异,即最有助于改进测试套件的变异。根据风险分布,若组件C中的某个变异能影响多个其他组件,则该组件风险较高,受影响的组件越多,风险越高。因此,应关注对其他组件影响最大的未检测到的变异。
目前,有两种衡量变异影响的方法:
1.
对代码覆盖的影响
:若变异导致程序执行不同路径,则更有可能改变程序行为。统计变异后覆盖情况发生变化的方法数量,数量越多,影响越大。
2.
对前置和后置条件的影响
:每个方法的参数和返回值都有一定条件,通过执行自动学习这些条件,并检查变异后这些条件是否被违反,违反的条件越多,变异的影响越大。
聚焦于影响最大的变异体,等效变异体比例较低(≤ 3%),且能指向程序中缺陷造成最大损害的位置。例如,上述AspectJ中的变异影响了多达21个条件,但未触发任何测试,这显然是需要改进测试套件的原因。
Javalanche框架
为进行变异测试实验,开发了用于Java程序变异测试的Javalanche框架。该框架实现了上述所有优化,能在合理时间内测试中等规模的程序。它还支持根据变异的影响对变异进行排序,方便关注影响最大的变异。
Javalanche框架可在网站上获取,作为一个框架,它易于扩展额外的操作符或影响检测器,是全自动的,可作为批处理工具在夜间运行,也有简单的Eclipse集成。若需要交互式工具,也可考虑µJava。
变异测试的发展
从第一篇关于变异测试的论文发表至今已有30年,如今变异测试才逐渐成熟,原因主要是自动化测试比10年前更广泛,而变异测试离不开自动化测试。
以注入缺陷的方式查找缺陷:精妙的变异测试
总结与展望
变异测试作为一种评估测试套件质量的强大方法,具有显著的优势和潜力。它能够深入挖掘测试套件的有效性,帮助我们发现那些常规测试难以察觉的问题。通过向程序中注入人工缺陷,变异测试可以模拟真实世界中的各种缺陷情况,从而更全面地检验测试套件的能力。
然而,变异测试也面临着一些挑战,如耗时较长、等效变异体的处理等。但通过采用高效的测试技术,如直接操作二进制代码、使用变异体模式和忽略未覆盖的代码,以及选择合适的变异操作符,我们可以在一定程度上克服这些挑战,提高变异测试的效率和实用性。
在实际应用中,我们可以按照以下步骤进行变异测试:
1.
选择合适的变异测试框架
:根据项目所使用的编程语言,选择如Mothra(FORTRAN程序)、µJava(Java程序)或Javalanche(Java程序)等框架。
2.
确定变异操作符
:依据项目需求和特点,选择合适的变异操作符,如替换数值常量、否定跳转条件等。
3.
执行变异测试
:利用选择的框架和操作符,对程序进行变异,并运行测试套件,记录未被检测到的变异。
4.
分析未检测到的变异
:使用影响评估方法,如对代码覆盖的影响和对前置后置条件的影响,确定最有价值的未检测到的变异。
5.
改进测试套件
:针对有价值的未检测到的变异,编写新的测试用例,改进测试套件。
以下是变异测试的流程图:
graph TD
A[选择变异测试框架] --> B[确定变异操作符]
B --> C[执行变异测试]
C --> D[记录未检测到的变异]
D --> E[分析未检测到的变异]
E --> F{是否有有价值的变异}
F -- 是 --> G[改进测试套件]
F -- 否 --> H[结束]
G --> C
变异测试的未来趋势
随着软件开发的不断发展,变异测试也将不断演进和完善。以下是一些可能的未来趋势:
1.
与人工智能的结合
:利用人工智能技术,如机器学习和深度学习,自动识别等效变异体,减少人工评估的工作量。同时,人工智能还可以帮助优化变异操作符的选择,提高变异测试的效果。
2.
更广泛的应用场景
:变异测试不仅可以应用于软件测试,还可以扩展到其他领域,如硬件设计、网络安全等。在这些领域中,变异测试可以帮助发现潜在的漏洞和缺陷,提高系统的可靠性和安全性。
3.
与持续集成和持续交付的集成
:将变异测试集成到持续集成和持续交付流程中,实现自动化的测试和部署。这样可以在软件开发的早期阶段及时发现和修复缺陷,提高开发效率和软件质量。
总之,变异测试作为一种精妙的测试方法,为我们提供了一种有效的手段来评估和改进测试套件的质量。虽然目前还存在一些挑战,但随着技术的不断进步,变异测试有望在未来发挥更大的作用,为软件开发的质量保障提供更有力的支持。
总结表格
| 方面 | 详情 |
|---|---|
| 变异测试优点 | 真正评估测试质量;反映高风险模块问题;复制缺陷分布 |
| 高效变异测试技术 | 直接操作二进制代码;使用变异体模式;忽略未覆盖代码 |
| 常用变异操作符 | 替换数值常量;否定跳转条件;替换算术操作符;省略方法调用 |
| 衡量变异影响方法 | 对代码覆盖的影响;对前置和后置条件的影响 |
| 未来趋势 | 与人工智能结合;应用场景扩展;与持续集成和交付集成 |
通过对变异测试的深入了解和应用,我们可以更好地保障软件的质量,减少软件中的缺陷,为用户提供更可靠的软件产品。希望本文能为你在变异测试方面的学习和实践提供有价值的参考。
超级会员免费看

被折叠的 条评论
为什么被折叠?



