什么是突变测试?(示例)

⚡ 智能摘要

变异测试故意在源代码中引入小的缺陷,然后针对每个有缺陷的版本运行现有的测试套件,以衡量这些测试是否足够强大,能够检测到这种变化。

  • 🔘 定义: 变异程序是指携带一个故意语法更改的程序,杀死它证明测试检测到了该更改。
  • ☑️ 过程: 生成变异体,对原始样本和变异体运行测试套件,比较输出结果,然后加强漏检错误的测试。
  • Opera目的: Operand 替换、表达修饰和语句修饰产生了三种主要的突变体家族。
  • 🧪 得分了: 变异得分是变异体被杀死的百分比,它衡量的是断言强度,而不是单纯的行执行。
  • 🛠️ 工具: Stryker 覆盖 Java脚本, TypeScript、C# 和 Scala,而 PIT 则在 Maven 内部修改 JVM 字节码,并且 Gradle 建立。
  • ⚠️ 成本权衡: 每个变异体都会重新运行整个测试套件,因此,如果没有自动化,变异测试速度慢、成本高、不切实际。

突变测试

什么是突变测试?

突变测试 变异测试是一种软件测试方法,它通过修改(或称变异)源代码中的某些语句,来检验测试用例是否能够发现源代码中的错误。变异测试的目标是确保测试用例的鲁棒性,使其在面对变异后的源代码时能够失败。

对变异程序所做的更改必须非常小,以免影响程序的整体目标。变异测试也称为基于缺陷的测试策略,因为它涉及故意在程序中创建缺陷。它是一种…… 白色 Box 测试与验证 主要应用于 单元测试.

变异测试最早由 Richard Lipton 于 1971 年在一篇学生论文中提出,并于 1978 年由 DeMillo、Lipton 和 Sayward 在论文《测试数据选择提示》中正式阐述。由于当时的计算成本较高,变异测试一度发展受阻,但此后随着诸如……等编程语言的出现,它又重新获得了关注。 Java,C#, Python, Java脚本和 XML。

如何执行变异测试?

以下是执行变异测试(也称为变异分析)的步骤:

第三步: 通过创建多个称为变异体的版本,可以在程序源代码中引入缺陷。每个变异体都应包含一个缺陷,目标是使变异体版本运行失败,从而验证测试用例的有效性。

第三步: 测试用例应用于原始程序和变异程序。 测试用例 应该是足够的,并且它被调整来检测程序中的错误。

第三步: 比较原始程序和变异程序的运行结果。

第三步: 如果原始程序和变异程序产生不同的输出,则变异程序会被测试用例判定为错误。因此,该测试用例足以检测出原始程序和变异程序之间的差异。

第三步: 如果原始程序和变异程序产生相同的输出,则变异程序得以存活。在这种情况下,需要创建更有效的测试用例来杀死所有变异程序。

下图 trac从最初的程序到变异体的产生,再到最终的死亡或存活判决,都遵循相同的五个步骤。

变异测试工作流程图,展示了原始程序、生成的变异体、测试执行过程以及变异体存活或死亡的判定结果。

如何创建变异程序?

变异是指对程序语句进行的一次语法更改。每个变异后的程序与原始程序之间应该恰好只有一个语法更改。

原创节目 变异程序
如果 (x>y)
打印“你好”
其他
打印“嗨”
如果(x
打印“你好”
其他
打印“嗨”

在上面的示例代码中,只有比较运算符发生了变化,但当 x 大于 y 时,测试用例现在会输出“Hi”而不是“Hello”。图示展示了这一语法上的修改。

对程序语句进行一次语法更改,即可产生一个单独的变异体。

突变程序中需要改变什么?

有多种技术可用于生成变异程序。以下三个类别涵盖了大多数工具自带的变异算子。

Operand 替换运算符 表达式修改运算符 语句修改运算符
将操作数替换为另一个操作数(x 替换为 y,或 y 替换为 x),或者替换为常量值。 在程序语句中替换运算符,或插入新运算符。 修改程序语句以创建变异程序。
计费示例:
如果 (x>y) 则替换 x 和 y 值
如果(5>y)用常数 5 替换 x
计费示例:
如果(x==y)
我们可以将 == 替换为 >=,并将变异程序表示为
如果(x>=y)并在语句中插入++
如果(x==++y)
计费示例:
删除 if-else 语句中的 else 部分
删除整个if-else语句,以检查程序的运行情况。

一些变异算子的示例:

  • GOTO 标签替换
  • 退货声明替换
  • 语句删除
  • 一元运算符插入(例如 - 和 ++)
  • 逻辑连接器更换
  • 可比较数组名称替换
  • 删除 if-else 语句中的 else 部分
  • 添加或替换运算符
  • 通过更改数据进行语句替换
  • 变量的数据修改
  • 程序中数据类型的修改

Opera与边界条件相交的拓扑结构最常存活,因此突变结果通常指向边界条件的缺口。 边值分析.

突变测试的类型

In 软件工程变异测试从根本上分为三种类型——语句变异、值变异和决策变异。

  • 语句修改 – 语句被剪切、粘贴或删除,因此结果可能是删除一些代码行。
  • 价值变异 – 修改主要参数和常量的值,例如改变循环边界或阈值。
  • 决策突变 – 控制语句发生更改,例如翻转ping 关系运算符或否定条件。

工具会将它们的操作符归入这三个类别,因此,产生存活变异体的操作符家族可以告诉测试人员缺少哪种类型的断言。存活的决策变异体通常标记一个未经测试的分支,这与……重叠。 循环测试.

突变测试的自动化

手动执行变异测试极其耗时且复杂,因此建议使用自动化工具,这样也能降低成本。变异测试工具会编译变异体,安排运行,记录每次测试失败导致哪个变异体被杀死,并报告测试得分。

可用工具列表:

  • 史赛克 — 一个开源的变异测试框架,提供多个版本 Java脚本和 TypeScript (StrykerJS)、C# 和 .NET (Stryker.NET) 以及 Scala (Stryker4s)。
  • 基坑也写作 PITest——一种用于突变检测的系统 Java 以及修改编译后的字节码并接入 Maven 的 JVM 和 Gradle 与……并肩而建 JUnit.

两者都作为构建步骤运行,因此它们属于同一步骤。 持续整合 管道其余部分 自动化测试 套房。

突变评分

突变得分定义为被杀死的突变体占突变体总数的百分比。

突变分数 = (杀死的突变体/突变体总数) * 100

下面显示的是大多数工具报告的公式形式。

突变得分公式为:用死亡的突变体数量除以突变体总数,再乘以一百。

当测试用例的得分达到 100% 时,则认为其变异性已得到充分满足。实际上,分母必须排除某些特定用例。 等效突变体 — 语法改变后行为与原代码完全相同的变异体,因此任何测试都无法将其判定为无效。因此,工具会将判定为无效的变异体数量除以无效变异体总数加上存活的非等效变异体总数,并允许测试人员标记等效变异体。

实验结果表明,变异测试是衡量测试用例充分性的有效方法。其主要缺点在于生成变异体以及针对每个变异体执行所有测试用例的成本较高。

突变检测与 Code 保障范围

测试覆盖率 并不能证明测试的有效性。行覆盖率和分支覆盖率记录的是哪些语句执行了,而不是之后是否进行了任何验证,因此,即使测试调用了一个方法但什么也没断言,它仍然会被计为覆盖。变异测试弥补了这一缺陷,因为只有当断言实际失败时,变异体才会被终止。

方面 Code 覆盖 突变评分
它测量什么 测试执行了哪些代码行或分支? 哪些注入故障被检测到
对断言很敏感 不——即使测试没有断言,它仍然可以增加覆盖率。 是的——只要没有断言失败,变异体就能存活下来。
运行成本 一次仪器化测试运行 每个存活的变异体只进行一次测试,目前速度较慢。
典型用途 每次提交时都要快速设置一个门控 对关键模块进行更深入的定期检查
故障模式 100%覆盖率,但未经任何实际验证 无法被杀死的同类变异体

这两个指标是互补的。覆盖率指的是从未被执行过的代码;变异分数指的是已执行过但从未被检查过的代码。两者都基于相同的信息。 缺陷管理流程同时采取以下措施: 缺陷密度.

突变测试的优势

以下是突变测试的优点:

  • 这是实现对源程序高覆盖率的有效方法。
  • 它测试测试套件本身,而其他任何测试套件都无法做到这一点。 软件测试技术 直接执行。
  • 变异测试为软件开发人员带来了良好的错误检测能力。
  • 该方法能够发现源代码中的歧义,并有可能暴露出普通运行永远无法触及的错误。
  • 幸存的变异体是可以采取行动的:每个变异体都指出了该套件未能注意到的特定行和特定更改。
  • 客户通过这项测试将获得更可靠、更稳定的系统,从而受益匪浅。

突变测试的缺点

另一方面,突变检测也存在以下缺点:

  • 变异测试成本极高且耗时,因为需要生成和编译大量的变异程序。
  • 由于这项测试非常耗时,可以说如果没有自动化工具,这项测试是无法完成的。
  • 每个变异体都要经过与原始程序相同数量的测试用例,因此必须对整个测试套件运行大量的变异体。
  • 同种变异体无法通过任何测试杀死,通常需要人工审核才能将其与真正的幸存者区分开来。
  • 由于该方法会改变源代码,因此不适用于 黑色 Box 测试与验证.

何时使用突变检测

上述成本分析表明,变异测试很少会在每次提交代码时都对整个代码库进行测试。它的优势在于,当未检测到的缺陷代价高昂,且被测代码量较小,能够快速发生变异时,变异测试才能真正发挥作用。

  • 安全关键或财务逻辑 — 付款计算、税收规则和授权检查,其中,一个无声的错误答案比崩溃更糟糕。
  • 套房覆盖率异常高 — 当覆盖率接近 100% 时,缺陷仍然存在。
  • 正在重构遗留代码 — 突变结果揭示了现有测试是否能够发现退化现象。
  • 库和共享组件 — 重复使用中的故障 元件 乘以每个呼叫者。
  • 团队练习 测试驱动的开发 — 该分数检验的是,首先编写的测试是否真正有效。

通常情况下,在一次性原型、缺乏分支逻辑的薄弱代码或生成的代码,或者以慢速测试为主的测试套件上运行测试是不值得的。 整合测试 单程就需要几个小时。

因此,大多数团队会将运行范围限定在已更改的文件上,为重要的模块设置阈值,并允许更广泛的操作进行。 回归测试 套房承载着其余部分 软件测试生命周期.

常见问题

等效变异是指改变语法但不改变行为的更改,例如替换永远不会到达的循环边界。任何测试都无法将其排除,因此必须先标记并排除此类变异,才能信任其得分。

没有统一的标准。团队通常会为支付或安全逻辑等关键模块设定较高的阈值,而其他模块则设定较低的阈值。与其追求百分比,不如审查高风险代码中每一个幸存的变异代码。

这项技术基于两个假设。第一个假设认为程序员编写的代码几乎都是正确的,因此真正的错误都很小。第二个假设认为能够捕获小错误的测试也能捕获由这些小错误衍生出的复杂错误。

将变异限制在当前分支中更改的文件,重用覆盖率数据以便只执行涉及变异体的测试,并行运行变异体,并在分数下降而不是绝对值下降时使构建失败。

机器学习模型预测哪些变异体可能存活下来,以便可以修剪运行,对可能的等效变异体进行分类以供审查,并生成类似于项目历史中看到的故障的变异体,而不是统一的操作员交换。

是的,就机制部分而言。给定一个存活的变异体和待测方法,Copilot 会生成缺失的断言或边界情况测试。审核人员仍然需要确认预期值是否正确,而不是简单地从当前行为复制而来。

它会审核结果。测试驱动开发在编写代码之前会先生成测试,但并不能保证这些测试能够充分验证问题。对相同的模块进行周期性变异测试,可以检验红绿循环是否真的生成了在错误答案上失败的测试。

不。故障注入会破坏运行时环境,而且 模糊测试 变异测试会输入格式错误的数据,并对应用程序进行评判。变异测试会改变源代码并检验测试套件,因此被评估的对象会发生变化。

总结一下这篇文章: