13. 变异测试基础¶
13.1. 变异体的类型¶
编写测试用例的主要目的是确保你的解答代码正常工作, 并产生你想要的行为。 变异测试中有不同的算子(mutator), 可以让你检查测试套件是否正确地测试了项目解答代码中的每个元素。 这是确保你的解答代码按预期逻辑工作的好办法。
使用变异测试的另一个好处是确保你的解答代码设计良好, 并遵循良好的编程实践。 这意味着你的解答代码没有不必要的复杂度、死代码(永远无法执行的代码)、 糟糕的设计、技术债等。
不同的算子保证了编程项目的不同品质。 对于本课程的编程项目,我们只使用两种算子来生成下列变异体(mutant):
算术运算变异体
逻辑表达式变异体(移除条件)
别担心,你不需要设置任何东西。 如果你正确安装了最新版本的 Web-CAT 提交插件, 那么你 Eclipse IDE 中的变异测试插件应该默认带有我们想要的那两个算子。
在接下来的章节中,我们将探讨:
这两种算子是什么。
它们如何工作。
它们产生什么样的反馈。
如何使用反馈中的 HINT(提示)。
如何编写测试用例来提高变异覆盖率。
使用这两种算子的不同示例。
13.1.1. 算术运算变异体(Arithmetic Operation Mutant)¶
算术运算变异体检查你的测试套件是否测试了算术运算。 该算子会把一个算术运算替换为其成员之一。 该算子由 2 个子算子 AOD_1 和 AOD_2 组成, 它们分别把运算变异为其第一个成员和第二个成员。
如果测试套件检测到了该改动——即代码发生变异后导致某个测试失败, 那么测试套件就算通过了。 术语上称测试套件"杀死了该变异"(killed the mutation)。 这会带来更高的变异覆盖率分数。 当测试套件未能检测到变异体时,变异覆盖率分数就会较低。 在这种情况下,我们需要编写额外的测试用例断言, 来测试所涉及的那个算术运算。
例如
int a = b + c;
将被变异为
1int a = b; // 替换为第一个成员
以及
1int a = c; // 替换为第二个成员
这里, b 是第一个成员, c 是第二个成员。
这样,变异测试就确保算术运算符的预期行为得到了测试。
13.1.2. 示例代码 1:算术运算变异体¶
作为一个例子,我们想编写一个函数:接收两个数字并返回它们的和。
1 public static int Addition(int num1, int num2) {
2 int sum = 0;
3
4 sum = num1 + num2; // --> 数学运算
5
6 return sum;
7 }
现在,如果我们执行变异测试,它会将代码变异如下:
用第一个成员替换算术运算:
1 public static int Addition(int num1, int num2) {
2 int sum = 0;
3
4 sum = num1; // --> 数学运算
5
6 return sum;
7 }
用第二个成员替换算术运算:
1 public static int Addition(int num1, int num2) {
2 int sum = 0;
3
4 sum = num2; // --> 数学运算
5
6 return sum;
7 }
如果没有任何测试用例执行这段代码, 测试过程将在 Mutations List 标签页的 LINES_NOT_TESTED 组下生成下列变异。 (未处理变异体的图标显示为"红色的 bug"。)
Figure 2.13.1: 示例代码 1:无测试用例时的算术运算变异体¶
在这里,我们可以看到 HINT(提示)建议我们编写测试用例断言, 以测试算术运算的预期行为。
下面的测试用例将杀死这两个变异体:
1 // testAddition 测试两个数相加
2 @Test
3 public void testAddition() {
4 // 测试 5+10 == 15
5 assertEquals(15, SimpleExample.Addition(5, 10));
6 }
如果我们再次运行变异测试,那么在 Mutations List 标签页的 LINES_NOT_TESTED 组下,将不会再出现之前的那些变异。
13.1.3. 逻辑表达式变异体(移除条件)¶
逻辑表达式算子(又称移除条件算子)检查你的测试套件是否正确测试了逻辑表达式。 该算子会把逻辑表达式替换为 TRUE 或 FALSE, 然后用变异体运行你的测试套件。
例如,把逻辑表达式替换为 TRUE 条件:
1if (a == b) {
2// 执行某些操作
3}
将被变异为
1if (true) {
2// 执行某些操作
3}
例如,把逻辑表达式替换为 FALSE 条件:
1if (a == b) {
2// 执行某些操作
3}
将被变异为
1if (false) {
2// 执行某些操作
3}
如果存在不止一个逻辑表达式,那么每个表达式都会在测试套件的单独一次运行中被变异。 逻辑表达式算子还会变异用于顺序检查(如 <=、>)的字节码指令。
如果同一条语句中存在不止一个逻辑表达式, 那么生成的变异体会按照语句中逻辑表达式的顺序排列。 请记住,对于多个逻辑表达式,你必须对每一个表达式都进行测试。
13.1.4. 示例代码 2:逻辑表达式变异体(移除条件)¶
作为一个例子,我们想编写一个函数:接收一个数字, 如果数字为正数则返回 TRUE,如果数字为零或负数则返回 FALSE。
1 public static boolean PositiveCheck(int number) {
2 if (number > 0) { // --> 真或假(2 种情况)
3 return true; // 正数
4 }
5 else {
6 return false; // 零或负数
7 }
8 }
执行变异测试会将代码变异如下。
使用 TRUE 替换逻辑表达式:
1 public static boolean PositiveCheck(int number) {
2 if (true) { // --> 真或假(2 种情况)
3 return true; // 正数
4 }
5 else {
6 return false; // 零或负数
7 }
8 }
使用 FALSE 替换逻辑表达式:
1 public static boolean PositiveCheck(int number) {
2 if (false) { // --> 真或假(2 种情况)
3 return true; // 正数
4 }
5 else {
6 return false; // 零或负数
7 }
8 }
如果没有测试执行这段代码, 它会在 Mutations List 标签页的 LINES_NOT_TESTED 组下生成下列变异。
未处理变异体的图标显示为"红色的 bug"。
Figure 2.13.2: 示例代码 2:无测试用例时的逻辑表达式变异体(移除条件)¶
为了消除这些变异,我们可以编写如下的测试用例断言:
1 // testEvenOddCheckWithEvenNumber 测试正数
2 @Test
3 public void testPositiveCheckWithPositiveNumber() {
4 assertTrue(SimpleExample.PositiveCheck(10));
5 }
6
7 // testEvenOddCheckWithOddNumber 测试零
8 @Test
9 public void testPositiveCheckWithZero() {
10 assertFalse(SimpleExample.PositiveCheck(0));
11 }
12
13 // testEvenOddCheckWithOddNumber 测试负数
14 @Test
15 public void testPositiveCheckWithNegativeNumber() {
16 assertFalse(SimpleExample.PositiveCheck(-5));
17 }
如果我们再次运行变异测试,那么在 Mutations List 标签页的 LINES_NOT_TESTED 组下,将不会再出现之前的那些变异。
13.1.5. 示例代码 3:一条语句中的多个变异体(EvenOddCheck)¶
编程语句中可以同时包含算术运算和逻辑表达式。 在这种情况下,变异测试会为每种类型返回对应的变异体, 并把它们列在 Mutations List 标签页下。
例如,我们想编写一个函数:接收一个数字, 如果数字为偶数则返回 TRUE,如果数字为奇数则返回 FALSE。
1 public static boolean EvenOddCheck(int number) {
2 if (number % 2 == 0) { // --> 算术运算(2 种情况);逻辑表达式(2 种情况)
3 return true; // 偶数
4 }
5 else {
6 return false; // 奇数
7 }
8 }
因此,它会在 Mutations List 标签页下生成下列变异:
Figure 2.13.3: 示例代码 3:无测试用例时一条语句中的多个变异体¶
为了消除这些变异,我们可以编写如下的测试用例断言:
1 // testEvenOddCheckWithEvenNumber 测试偶数
2 @Test
3 public void testEvenOddCheckWithEvenNumber() {
4 assertTrue(SimpleExample.EvenOddCheck(10));
5 }
6
7 // testEvenOddCheckWithOddNumber 测试奇数
8 @Test
9 public void testEvenOddCheckWithOddNumber() {
10 assertFalse(SimpleExample.EvenOddCheck(5));
11 }
如果我们再次运行变异测试,那么在 Mutations List 标签页的 LINES_NOT_TESTED 组下,将不会再出现之前的那些变异。
13.1.6. 示例代码 4:循环条件(可选)¶
循环中包含需要测试的逻辑表达式。 不过,测试循环条件通常是可选的,并不会对变异覆盖率产生负面影响。
例如:
1for (int i = 0; i < 10; i++)
请记住,如果你不测试 for 循环的终止条件, 你的代码可能会陷入无限循环。
