聊聊高大上的变异测试

3,910 阅读2分钟

我正在参加「掘金·启航计划」

一、变异测试简介

变异测试**是一种基于故障注入的测试技术****,**将错误代码插入到被测代码中,以验证当前测试用例是否可以发现注入的错误。我认为该测试手段理论上属于白盒测试范畴。

变异测试的主要目的是为了验证测试用例的有效性,在注入变异后,测试用例能发现该错误,则表明用例有效的;反之,表明测试用例是无效的,需要补充该变异的测试用例。

变异测试有助于评估测试用例的质量,以帮助测试人员编写更有效的测试用例。测试人员设计的测试用例发现的变异体越多,表明其设计的测试用例质量就越高。

在深入理解变异测试之前,让我们先搞懂和它相关的几个核心概念。

1) 变异

可以理解为对源代码的任何更改,也可以理解为引入的故障。

2) 变异体

可以理解为被测代码的变异版本,即已经在被测代码中注入变异的代码。当测试用例在变异体版本的代码运行时,理论上该测试用例执行的结果应该与原被测代码执行的结果不同。

  • 存活的(survived)变异体: 变异注入的错误并不能被测试用例感知,这种情况称为变异体能够“存活”,说明测试用例的有效性存在问题,需要对测试用例进行补充和修正。

  • 杀死的(killed)变异体: 在变异体代码上执行测试不通过,说明变异注入的错误能够被测试用例T感知到,测试用例能够“杀死”此变异,说明此测试用例是有效的。

  • 等价的变异体:这个其实也很好理解,通过下面例子介绍下。

    源代码: for(int i=0;i<10; i++){ // 源程序 //To-do ... } 变体1: for(int i=0;i!=10; i++){ //变体1 //To-do ... } 变体2: for(int i=0;i<10; i--){ //变体2 //To-do ... }

其中变体1与源代码是等价的:都是i从0开始,经历1,2,3,4,5,6,7,8,9到10,在源代码中由于10<10返回False,退出循环;在变体1中由于10!=10返回False,退出循环。

3) 变异分

变异分也称为变异充分性,这是基于测试用例发现变异体数量计算出来的分数,计算公式如下:

注意,在计算变异分时不会考虑等价的变异体。

二、如何进行变异测试

下面通过JavaScript代码做演示,使用Jasmine测试框架写单测用例。

1.编写原被测代码

const user_info = () =&amp;amp;gt; {
  mother_age = parseInt(prompt("Enter mother's age"))
  daughter_age = parseInt(prompt("Enter daughter's age"))

  if (mother_age &amp;amp;gt; daughter_age) {
    alert(`Daughter's age is ${daughter_age}. Mother's age is
    ${mother_age}. Welcome to the Mother-Daughter program`)

  } else {   
    alert(`Daughter's age: ${daughter_age}, is more than mother's age: ${mother_age}.
    Please enter correct ages`)      
  }
}
user_info();

2.编写 Jasmine 单元测试用例

describe("User", function() {
  it("should compare the two numbers from user input", function(){
    expect(20).toBeGreaterThan(5);
  })
});

**3.**在原始代码运行测试,以确保测试用例都是通过的

Daughter's age is 5. Mother's age is 20. Welcome to the Mother-Daughter program

4.注入变异

我们将"**>"运算符 (mother_age >daughter_age) 更改为"<"**运算符 (mother_age <daughter_age)

const user_info = () =&amp;amp;gt;{   mother_age = parseInt(prompt("Enter mother's age"))   daughter_age = parseInt(prompt("Enter daughter's age"))        if (mother_age &amp;amp;lt; daughter_age) {    alert(`Daughter's age is ${daughter_age}. Mother's age is    ${mother_age}. Welcome to the Mother-Daughter program`)        } else {       alert(`Daughter's age: ${daughter_age}, is more than mother's age: ${mother_age}.    Please enter correct ages`)         }}  user_info();

5.在变异版本上运行测试

Daughter's age: 5, is more than mother's age: 20. Please enter correct ages

6.比较源代码和变异版本代码运行测试的结果

通过运行结果发现,虽然二者使用相同的测试用例,但是运行结果是不一样的,因此,我们的测试用例“杀死”了变异,也说明我们的测试用例是有效的。

三、变异测试类型

1) 值变异

通过改变参数值来实现代码变异,例如在原值基础上+/- 1。

原始代码

let arr = [2,3,4,5]for(let i=0; i&amp;amp;lt;arr.length; i++){   if(i%2===0){    console.log(i*2)   }        }

上面的代码逻辑是将i<4的偶数相乘,那么通过值变异可以将初始化值由let i=0更改为let i=1

变异代码

let arr = [2,3,4,5]for(let i=1; i&amp;amp;lt;arr.length; i++){   if(i%2===0){    console.log(i*2)   }    }

2) 语句变异

通过删除、重复或颠倒代码块中的语句实现代码变异。例如,在 if-else 语句块中,重复console.log代码。

原始代码

let arr = [2,3,4,5]for(let i=0; i&amp;amp;lt;arr.length; i++){   if(i%2===0){    console.log(i*2)        }    }

变异代码

let arr = [2,3,4,5]for(let i=0; i&amp;amp;lt;arr.length; i++){   if(i%2===0){    console.log(i*2)    console.log(i*2)   }}   

3) 运算符变异

通过修改代码中的运算符来实现代码变异,例如常见的值比较逻辑。我们可以将**> 更改为 <**。

原运算符

运算符变异

1

<=

>=

2

>=

==

3

===

==

4

and

or

5

||

&&

四、变异测试优缺点分析

优点:

  • 可以覆盖大部分代码逻辑。
  • 可以测试特定部分代码,而不仅仅是通过路径、分支或语句方式实现。
  • 可以帮助我们评估测试用例的质量并进行优化。

缺点:

  • 变异测试需要在单元测试已经做得比较完备的基础上才有其价值。

五、变异测试工具推荐

Stryker、Jumble、PIT 和 Insure++。