28、以注入缺陷的方式查找缺陷:精妙的变异测试

以注入缺陷的方式查找缺陷:精妙的变异测试

在软件开发中,当一个项目的开发周期接近尾声,所有新功能都已完成且所有测试都通过时,作为测试经理,你是否就可以决定发布软件了呢?实际上,即便所有测试都通过,也不能掉以轻心,因为测试套件可能并不够好。那么,如何检验测试套件发现缺陷的有效性呢?变异测试提供了一种简单、优雅且有效的方法。

评估测试套件质量

工程师和管理者喜欢量化事物,测试套件的质量也不例外。为了衡量测试套件的质量,人们开发并应用了各种覆盖指标,常见的有代码覆盖、分支覆盖和条件覆盖。
- 代码覆盖 :检查测试套件中是否执行了各个语句。若测试未执行某语句,而该语句存在触发失败的缺陷,那么这个缺陷在测试阶段就无法被发现。
- 分支覆盖 :确保每个分支至少被执行一次。
- 条件覆盖 :保证每个(子)条件分别被评估为真和假一次。

然而,这些测试质量指标存在一些问题:
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. 与持续集成和持续交付的集成 :将变异测试集成到持续集成和持续交付流程中,实现自动化的测试和部署。这样可以在软件开发的早期阶段及时发现和修复缺陷,提高开发效率和软件质量。

总之,变异测试作为一种精妙的测试方法,为我们提供了一种有效的手段来评估和改进测试套件的质量。虽然目前还存在一些挑战,但随着技术的不断进步,变异测试有望在未来发挥更大的作用,为软件开发的质量保障提供更有力的支持。

总结表格
方面 详情
变异测试优点 真正评估测试质量;反映高风险模块问题;复制缺陷分布
高效变异测试技术 直接操作二进制代码;使用变异体模式;忽略未覆盖代码
常用变异操作符 替换数值常量;否定跳转条件;替换算术操作符;省略方法调用
衡量变异影响方法 对代码覆盖的影响;对前置和后置条件的影响
未来趋势 与人工智能结合;应用场景扩展;与持续集成和交付集成

通过对变异测试的深入了解和应用,我们可以更好地保障软件的质量,减少软件中的缺陷,为用户提供更可靠的软件产品。希望本文能为你在变异测试方面的学习和实践提供有价值的参考。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值