AST@ICSE 2021 https://arxiv.org/abs/2103.08480

1. 背景

突变测试是一种软件测试技术,用来衡量测试套件的有效性。尽管它已经存在了几十年,并在研究中得到了深入的研究,但它仍然没有被广泛地应用于工业。

极端突变测试是突变测试的一种变体,由Niedermayr等人于2016年引入(Will my tests tell me if i break this code?)。这种变化似乎很有发展前景,因为它的计算成本较低,并且由于其较高的抽象级别而更容易理解。

本文研究了极端突变测试相对于传统突变测试的时间改进有多大,它们在实践中的意义,以及目前的影响因素。在半导体行业的一个大型软件项目中执行了两种突变测试技术,并采访了两位经验丰富的开发人员,了解他们的软件测试实践,特别是极端突变测试的发现。

2. 相关研究

2.1 Mutation testing

对于Mutation test,在被测试软件的源代码中引入受控的小变化来改变其行为。

  • mutators :这种类型的变化称为突变。
  • mutants :相应的修改过的软件版本称为变种。

然后,重新运行给定的测试套件。如果没有测试失败,这意味着测试套件无法检测到突变。因此,据说突变体存活了下来。否则,据说突变体被杀死了。
一些软件变更可能产生语法上等价的突变体,即没有不同行为的突变体。这些突变体称为等效突变体。

突变测试的目标是通过增强或编写新的测试来杀死存活的非等效突变体,从而增强测试套件。被杀死的突变体与所有产生的非等效突变体的比率称为突变分数。因此,突变测试的目标通常通过将突变分数优化为1。

2.2 Extreme mutation testing

与传统突变测试(突变通常在指令级执行)不同,极端突变测试在方法层面执行这些突变。

Extreme mutation test 的目标是找到伪测试方法(Pseudo-tested methods),这些方法可以删除它们的全部功能,但没有测试会失败。 伪测试方法非常普遍。在另一项研究中,对19个开源项目进行了分析,伪测试方法占变异方法总数的中位数为10.1%。

特定的突变体必须存活下来才能将方法归类为伪测试。

  • 对于没有返回值的方法(void methods),mutator 清空整个方法的主体。如果产生的突变体存活下来,该方法被归类为伪测试。

  • 对于具有返回值的方法,通常需要多个突变体才能存活。例如,对于基本数据像Boolean,必须有两个突变体(mutator 分别用return true, return false 替换方法体)存活。如果两个突变体都存活下来,则该方法被归类为伪测试方法。如果只有一个幸存下来,则该方法被归类为部分测试(partially-tested

  • 类似地,选择使用其他数据类型(如Integer(0和1)或 String(如 “ “和 “A” )的默认返回值替换整个方法体的其他变体,这样,如果变体存活,则该方法可以归类为伪测试。从类(如对象)派生的复杂数据类型可以简单地设置为null。使用默认值的选择取决于使用的突变测试工具。

与传统的突变测试相比,极端突变测试的优势在于生成的突变数量要少得多,因为方法的数量明显低于可以突变的指令的数量。这减少了分析突变的运行时间和时间。另一方面,极端变异测试并不像传统变异测试那样细粒度地发现测试套件中的弱点。

3. 实验

3.1 Research questions

**

  • RQ1: Are the improved execution and analysis times of extreme mutation testing relevant in practice?

  • RQ2: How do extreme mutation testing results impact the established practice of writing unit tests with code coverage?

  • RQ3: Which factors prevent extreme mutation testing from industry adoption?

3.2 Mutation testing

数据集:

我们为半导体测试行业中使用的软件项目运行了传统和极端变异测试。该软件通过11000多个单元测试进行测试,这些单元测试调用了大约2000个方法(大约12500行代码)。软件项目是用Java编写的,由多个不同的小型项目组成。

3.3 Interview with developers

我们采访了两位具有七年以上开发经验的开发人员,他们熟悉测试中的软件。他们中没有人事先使用过突变测试,只有一位开发者只听说过。

4. 讨论与评估

4.1 实验结果

表一显示了11424个单元测试的PIT突变测试结果,这些单元测试通常在12秒内完成。对于传统的突变测试,我们使用PIT提供的两组突变。

  • default :集合由七个变异体组成,
  • all :集合由66个变异体组成。

image.png

表二显示了方法总数和伪测试方法的数量及其比例。它还显示了测试套件覆盖的行,以及总行数。在分析的软件项目中,2041种方法中有291种(14%)是伪测试方法。291个伪测试方法由1129行代码组成,其中835行覆盖。
image.png
作者对25种方法进行了更深入的分析,发现了方法被判定伪测试方法的三个原因:

  • Weak tests with no assertions (8)

    我们发现了八种没有断言的测试,这导致了它们被伪测试。例如,处理网络连接的类通过打开和关闭连接进行测试,但从未通过计算关闭前后打开的会话数来验证连接是否正常工作。

    其中一些测试可以通过简单地捕获输出并添加断言语句来修复。另一些除了向测试中添加断言外,还需要向被测类添加新方法。

  • Incomplete tests (3)

    我们还发现了三个不完整的测试。这些测试设计得很好,具有强大的断言,但没有检查类的某些属性。例如,对创建表的类进行了测试,以确定它是否写入了正确的数据行,但未测试表头(尽管被调用)。通过为表头添加另一个断言,可以很容易地解决这个问题。

  • Side-effect methods (14)

    其余14种方法是在测试期间调用的方法,但不受测试的影响,也不会对测试结果产生影响。因此,我们命名了最后一类副作用。典型的副作用方法是,例如,负责日志记录的自定义方法或存储元数据的方法,这些元数据从未在其他地方被测试套件使用或验证过。

4.2 结果评估

4.2.1 Answer to RQ1 – Relevance of execution and analysis time

使用七个默认突变子的传统突变测试所需时间几乎是极端突变测试(13分钟)的三倍(37分钟),尽管只有大约三分之一的突变子(见表一)。

表一显示,与传统方法(2176)相比,极端突变测试的粗粒度方法仅产生五分之一的存活突变体(409)。

当在方法级别工作以检查哪些方法是伪测试的时,这个数字进一步降低到291(见表II)。这些数字在实践中更具相关性。如果我们假设一个伪测试方法只需5分钟就可以修复,那么即使修复291个伪测试方法也需要大约24小时。对传统方法的所有2176个突变体应用相同的计算将导致大约7天的分析。因此,我们得出结论,改进的执行和分析时间对于开发人员的执行时间和工作负载来说是相关的,并且在实践中是显著的。

4.2.2 Answer to RQ2 – Impact on established testing practices

极端变异测试补充了编写单元测试和代码覆盖率。编写单元测试主要是为了验证代码是否按预期工作,并作为未来代码更改的回归测试。

伪试验方法被视为与改进这两种方法相关。仅行覆盖率并不能准确描述测试套件的有效性。我们发现了1129行代码(见表II),在这些代码中,我们可以很容易地引入新的bug,而这些bug将不会被给定的测试套件注意到。事实上,表二显示,当将伪测试方法计算为“未覆盖”时,线路总覆盖率必须从89%调整为82%。

4.2.3 Answer to RQ3 – Industry adoption

尽管在执行和分析时间方面具有上述优势,及其对已建立的软件测试实践的积极影响,但极端变异测试相对较年轻,只有少量可用的实现,因此尚未广为人知。我们注意到有两个方面需要改进以促进行业采用:工具和使用。

对于工具,我们发现,通过突出显示伪测试代码,极端变异测试的结果将最好地体现在代码覆盖率报告中。
笛卡尔引擎目前不支持这一点。这种方法的优点是,开发人员已经熟悉覆盖率报告,并且很容易实现。

对于使用,进一步的研究应该调查极端变异测试必须如何改进,以便它可以在编写单元测试时运行。这是最有可能解决问题的时间点。此外,我们还观察到,在实践中使用极端突变测试时,突变分数是不相关的,因为有些方法不值得修复的。