生成突变体
变异算子
我们通常使用突变算子来生成突变体
操作员可能是,例如:
用 * 替换 +
用另一个变量 y 替换表达式中的变量 x
用另一个变量替换赋值的 LHS 上的变量 x变量 y
删除一个声明
在计算机编程中,术语“LHS”代表“左侧”。 它是指出现在赋值运算符 (=) 左侧的赋值语句部分。 LHS 是您指定要为其分配新值的变量或内存位置的位置。
例如,考虑以下赋值语句:
x = 10
在这种情况下,LHS 是变量“x”。 它是将存储值 10 的目标位置。
如果您想用另一个变量“y”替换 LHS 上的变量“x”,您可以将赋值语句修改为:
y = 10
现在,变量“y”成为赋值的 LHS,值 10 将存储在与“y”关联的内存位置。
一阶突变体First Order Mutants
(1) 一个突变体是一个一阶突变体,如果它可以通过一次应用一个突变算子产生。
(2) 通常只使用一阶突变体:这限制了产生的突变体数量。
(3) 如果一个突变体不是一阶突变体那么它就是一个高阶突变体。
(4) 一阶突变体的使用通常通过以下方式证明是合理的:
耦合效应:如果我们杀死一阶突变体,那么我们就杀死了(大多数)高阶突变体
有能力的程序员假设:真实代码接近于正确(因此类似于正确的一阶突变体在程序里面)
(5) 这些假设的一些证据——但证据有限
Example
We might mutate this
x=x+1; if (x>0) y=y+1; else y=y+2;
I To form this
x=x+1; if (x>0) y=y-1; else y=y+2;
I Or this
x=x+1; if (x>0) y=x+1; else y=y+2;
I Or
x=x+1; if (x>=0) y=y+1; else y=y+2;
杀死一个变种体
假设突变体 p0 是通过突变 p 的程序语句 s 形成的。
然后杀死 p0 我们需要找到输入:
(1) 执行:驱动程序执行到s。
(2)感染:导致s后不同的状态(即不同的一些变量的值,可能是程序指针)。
(3) Propagation:传播这种状态上的差异,形成不同的输出。
PIE框架
PIE 框架,在突变测试的背景下,代表“潜在感染效应”。 它是一种系统方法,用于确定代码中的特定突变是否被相应的测试用例“杀死”。
- 潜力:第一步是确定代码中发生突变的可能性。 突变通常是通过对原始代码进行小的更改来引入的,例如更改运算符、删除语句或修改常量。
- 感染:引入变异后,下一步就是将变异代码应用到现有的测试套件中。 变异的代码与测试用例一起执行,以确定它们中的任何一个是否能够“感染”变异。 换句话说,测试用例应该能够触发由突变引起的改变的行为。
- 效果:最后,通过比较变异代码的行为与原始代码的行为来分析变异的效果。 如果原始代码和变异代码的测试用例结果不同,则表明变异已被测试用例“杀死”,因为它检测到更改并产生了不同的输出或行为。
PIE 框架有助于系统地评估测试用例在检测突变方面的有效性。 目标是确保测试套件有效捕获突变,从而提高软件的整体质量和可靠性。
Recall the following example, and assume that values for x and y are output.
x=x+1; if (x>0) y=y+1; else y=y+2;
The following mutant
x=x+1; if (x>0) y=y-1; else y=y+2;
被 x = 1, y=11 的输入终止,但不被 x = -1, y=11 终止
这里的分支或语句覆盖也足够了。
Recall the following example, and assume that values for x and y are output.
x=x+1; if (x>0) y=y+1; else y=y+2;
The following mutant
x=x+1; if (x>=0) y=y+1; else y=y+2;
为了确定这个突变体是否被测试用例杀死,我们可以考虑两种情况:
-
当 x = -1 且 y = 11 时:
- 原代码中,由于x小于0,执行else分支,y变为11 + 2 = 13。
- 在突变代码中,x 仍然小于 0,但条件 (x >= 0) 为真。 因此,执行 if 分支,y 变为 11 + 1 = 12。
- 原始代码和突变体代码的输出不同,表明突变体已被该测试用例杀死。
-
当 x = 1 且 y = 11 时:
- 在原始代码和变异代码中,x 都大于 0,因此执行 if 分支。 在这两种情况下,y 都变为 11 + 1 = 12。
- 原始代码和突变体代码的输出相同,表明突变体没有被这个测试用例杀死。
现在,让我们解决为什么分支或语句覆盖率可能无助于指导我们进行这样的测试用例:
分支覆盖旨在确保条件语句的每个分支至少执行一次。 在这种情况下,原始代码和变异代码都具有相同的分支和相同的条件。 因此,仅实现分支覆盖并不能区分原始代码和变异代码的行为。
同样,语句覆盖旨在确保代码中的每个语句至少执行一次。 在这种情况下,原始代码和变异代码都具有相同的语句。
因此,仅仅依靠分支或语句覆盖不会引导我们找到可以区分原始代码和变异代码行为的特定测试用例。 为了检测两者之间的差异,我们需要涵盖 x 为负(在本例中为 -1)的特定条件的测试用例。
覆盖面和充分性标准Coverage and Adequacy Criterion
(1)假设我们形成一个程序 p 的突变体集合 M。
(2) 我们有一个相关的覆盖率度量:M 中被测试套件杀死的突变体的百分比。
(3) 有一个相关的充分性标准:我们需要杀死所有突变体。
等效突变体
(1) 如果没有可能的输入杀死 p0,则 p 的突变体 p0 是等价突变体。
(2) 则 p0 在句法上不同于 p 但在语义上相等的。
在突变测试中,等效突变体是原始代码的突变版本,其行为方式与所有可能输入的原始代码相同。 换句话说,等效突变体是不会改变程序的可观察行为的突变体。
为了进一步解释这个概念,让我们考虑一个例子:
原始代码:
def calculate_square(x):返回 x * x
突变代码(p0):
def calculate_square(x):返回 x ** 2
** 运算符在 Python 中用于求幂。 它还可以与其他整数或浮点指数一起使用。 比如:
x**3计算x的立方
,x**0.5计算x的平方根
。
在此示例中,原始代码通过将给定数字与自身相乘来计算给定数字的平方。 突变代码 p0 改为使用求幂运算符计算平方。
尽管 p0 在句法上与原始代码不同,但在语义上是等价的。 对于任何可能的输入,原始代码和变异代码都会产生相同的结果。 因此,没有可能的输入可以杀死突变体 p0,因为它的行为与原始代码相同。
从测试的角度来看,等效突变体很有趣,因为它们表明对代码的某些更改不会影响其功能。 然而,他们也强调了突变测试的局限性,因为它可能无法检测到此类突变体。
重要的是要注意,等效突变体的概念取决于程序行为的定义和用于确定正确性的 oracle。 在某些情况下,突变体 p0 可能被认为是等效的,而在其他情况下,它可能不是。
(3) 等价性可能更复杂——它可能取决于突变发生的上下文
i f ( x>y ) y=y+1; e l s e x=x+1;
a=x ;
w h i l e ( x>y ) y=y+x ;
b=y+1;
问题
这些会导致问题,因为:
(1) 我们无法杀死它们:理想情况下我们将覆盖更新到被杀死的非等价突变体的百分比。
(2) 然而,除非我们检测到等效的突变体,否则我们无法测量这种覆盖率。
(3) 等价性不可判定。
通常,求助于人工测试人员——但这很昂贵
模拟其他测试标准
(1) 通过适当的变异算子,我们可以模拟(或包含subsume)一些测试标准。
(2) 为了模拟另一个标准,我们需要产生一组突变体,使得:
如果我们杀死所有非等价的突变体,那么我们也必须满足初始标准。
(3) 我们可以使用下面的变异算子:
用“崩溃”替换语句(例如除以 0)。
(4)如果一个突变体被杀死那么测试必须执行相应的声明。
(5) 然后:如果我们产生所有这样的突变体并且我们的测试将它们全部杀死,我们的测试必须提供 100% 的语句覆盖率。
(6) 这取决于变异算子的选择。
(7) 然而,我们可以看到杀死所有突变体的标准可以包含经典的覆盖标准
经典的覆盖标准是用于通过测量代码在测试期间被执行的程度来评估测试用例的充分性的指标。 这些标准旨在确保代码的不同部分已被执行或被测试用例覆盖。 一些常见的经典覆盖标准包括:
-
语句覆盖率:该标准确保代码中的每条语句在测试期间至少被执行一次。 它测量测试套件覆盖的语句的百分比。
-
分支覆盖:分支覆盖旨在确保代码中所有可能的分支或决策点都已被执行。 它测量在测试期间采用或不采用的分支的百分比。
3.路径覆盖:路径覆盖旨在覆盖代码中所有可能的执行路径。 它确保分支和循环的每个可能组合都经过测试。
-
条件覆盖:条件覆盖检查布尔条件(真和假)的所有可能结果是否已在测试期间得到评估。 它确保已执行代码中的每个条件。
-
函数/方法覆盖率:函数或方法覆盖率测量在测试期间被调用或调用的函数或方法的百分比。
这些经典的覆盖标准提供了测试套件执行代码的程度的定量度量。 它们有助于识别未经过充分测试的代码区域,可能表明测试覆盖率存在差距。
实际问题
(1) 我们已经看到突变测试可以包含经典的覆盖标准。
(2)杀死突变体和找茬之间有更清晰的联系。
(3)可以直接测试生成:我们生成杀死突变体的测试用例。
(4)然而:
突变测试没有被广泛使用(但它被使用)。
主要用于评估测试生成技术的有效性
(5) 即使是小程序也可能有很多变异体。
我们需要生成并编译所有这些。
我们对它们运行测试用例。
(6) 等价突变体会使报道产生误导和浪费精力。
理想情况下,我们希望消除这些。
一个困难的过程——通常是手动的,如此昂贵且容易出错(特别是如果我们有很多突变体!)。
(7) 我们可能会使用一个子集,而不是使用整套建议的变异算子。
(8) 人们对运营商的子集感兴趣,例如:
它们导致突变体数量显着减少,实现线性。
如果我们杀死使用选定集生成的所有突变体,我们也会杀死几乎所有使用完整集生成的突变体。
(9)这种方法称为选择性突变。
(10)注意:从一组突变体中随机抽样也有帮助。
(11) 基本思路:我们用一个程序来捕获所有的突变体。
(12) 可以通过使用“切换”开/关突变的参数来实现。
(13)这减少了编译时间和占用的空间
突变体。
例子:
Recall the following example.
x=x+1; if (x>0) y=y+1; else y=y+2;
The following mutant
x=x+1; if (x>0) y=y-1; else y=y+2;
We can introduce parameter mut to control whether the mutation occurs.
x=x+1; if (x>0) {if (mut) y=y-1; else y=y+1;}
else y=y+2;
变异代码中引入了 mut
参数, 允许在代码中进行受控突变。 使用 mut
参数的目的是有选择地启用或禁用突变,从而便于比较原始代码和突变代码之间的行为。
通过引入 mut
参数,我们可以创建两种不同的场景:
-
当
mut
设置为True
时:- 如果条件
(x>0)
为真,变异代码y=y-1;
将被执行。 - 此更改改变了原始行为,因为它从“y”中减去 1 而不是加 1。
- 此场景有助于评估测试套件检测引入突变的能力。
- 如果条件
-
当
mut
设置为False
时:- 如果条件
(x>0)
为假,变异代码y=y+1;
将被执行。 - 此更改保留了原始行为,因为它将 1 添加到“y”。
- 此场景用作控制案例,以确保测试套件不会错误地将不存在突变标记为检测。
- 如果条件
通过切换 mut
参数的值,我们可以有选择地激活或停用变异,从而允许我们比较原始代码和变异代码在不同条件下的行为。 这种受控方法提供了一种方法来评估测试套件在检测引入的特定突变方面的有效性。
(14) 等效突变体会降低措施的价值,并会增加成本。
(15)解决这个问题的可能方法包括:
使用定理证明器、模型检查器或 SMT 求解器来消灭他们。
使用例如 依赖分析以避免产生一些等效突变体。
定义检查等价性的简单规则(例如比较编译代码)。
停止具有高(不是 100%)覆盖率的测试。
避免创建许多等效突变体的运算符
依赖分析是一种静态程序分析技术,用于识别和分析不同程序元素(例如变量、语句或指令)之间的依赖关系。 它旨在确定这些程序元素之间的关系和数据流。
依赖性分析的主要目标是了解一个程序元素的变化如何影响代码中的其他元素。 通过识别依赖关系,可以进行优化、检测潜在问题并避免在突变测试期间生成某些类型的等效突变体。
以下是依赖性分析的几个关键方面:
1.数据依赖:数据依赖是指程序中变量或表达式的值之间的关系。 它确定对一个变量或表达式的更改如何影响其他变量或表达式的计算或值。
2. 控制依赖:控制依赖是指不同语句或代码块之间的执行顺序和控制流依赖关系。 它根据条件、循环和函数调用等控制流结构确定执行路径。
3. 流依赖:流依赖分析程序内不同语句或指令之间的数据流。 它根据执行顺序和控制流捕获依赖关系。
4. 循环依赖:循环依赖分析特别关注循环内的依赖关系,因为循环通常具有复杂的控制和数据流交互。 它识别可能影响循环优化和并行化的依赖关系。
5.
通过执行依赖性分析,可以识别和理解程序中的依赖性。 该知识可用于避免生成某些类型的等效突变体,这些突变体不会由于已识别的依赖关系而影响程序的行为。 它有助于减少冗余或不相关突变体的数量,在突变测试期间节省时间和精力,并专注于更有意义的突变。
主题变奏Variations on a Theme——语言
(1) 可以改变模型或规范——而不仅仅是代码。
(2) 模拟不同类型故障的潜力。
(3) 如果模型是可执行的(或可以映射到代码),则有帮助。
(4) 或者,如果存在不可执行的语义就足够了(因此我们可以推断是否可能有共同的输出)。
主题变奏——杀戮的其他概念Other notions of killing
弱杀死突变体的概念已被定义:
如果突变语句被执行,这导致不同的状态。
所以:需要执行和感染但不需要传播。
传统定义则称之为强杀strong killing。
变异测试工具
(1)通常这些将:
产生突变体。
对突变体执行测试用例并确定哪些被杀死。
报告 例如: coverage。
(2)一些工具可能旨在生成测试用例以杀死突变体:
非常多的研究问题。
通常只有原型prototypes 可用
(3)有多种编程语言的工具。
有专业的工具。
有许多原型研究工具。
(4) 对于Java,示例包括以下(还有其他):
PIT
Major
µJava(可能是第一个工具)
(5)也是一个比较通用的工具,可以用于任何具有元模型的语言:Wodel 工具(和语言)。