关闭一个错误和变异测试

时间:2012-04-30 11:43:56

标签: c# algorithm il mutation-testing ninja-turtles

在为我最喜欢的突变测试框架(NinjaTurtles)编写“Off By One”变异测试程序的过程中,我编写了以下代码,以便提供检查我的实现的正确性的机会:

public int SumTo(int max)
{
    int sum = 0;
    for (var i = 1; i <= max; i++)
    {
        sum += i;
    }
    return sum;
}

现在这看起来很简单,并没有让我觉得尝试改变IL中的所有文字整数常量会有问题。毕竟,只有3个(01++)。

WRONG!

在第一次运行中它变得非常明显,它在这个特定的实例中永远不会起作用。为什么?因为将代码更改为

public int SumTo(int max)
{
    int sum = 0;
    for (var i = 0; i <= max; i++)
    {
        sum += i;
    }
    return sum;
}

仅将总和加0(零),这显然没有效果。不同的故事,如果它是多组,但在这种情况下,它不是。

现在有一个相当简单的算法来计算整数之和

sum = max * (max + 1) / 2;

我可以很容易地使突变失败,因为从任何一个常数中加1或减1都会导致错误。 (鉴于max >= 0

因此,针对这种特殊情况解决了问题。虽然它没有做我想要的突变测试,这是为了检查当我丢失++时会发生什么 - 实际上是一个无限循环。但这是另一个问题。

所以 - 我的问题:是否有任何琐碎或非平凡的情况,从0或1开始的循环可能导致无法重构的“一个突变”测试失败(代码在测试或测试中)以类似的方式? (请举例)

注意 :在应用突变后测试套件通过时,突变测试失败。

更新:一个不那么重要的事情的例子,但仍然可以重新测试以使其失败的内容将是以下

public int SumArray(int[] array)
{
    int sum = 0;
    for (var i = 0; i < array.Length; i++)
    {
        sum += array[i];
    }

    return sum;
}

如果您提供的测试输入为var i=0,则在将var i=1更改为new[] {0,1,2,3,4,5,6,7,8,9}时,针对此代码进行的变异测试会失败。但是,将测试输入更改为new[] {9,8,7,6,5,4,3,2,1,0},并且突变测试将失败。因此,成功的重构证明了测试。

4 个答案:

答案 0 :(得分:4)

我认为使用这种特殊方法,有两种选择。您要么承认它不适合突变测试,因为这种数学异常,或者您尝试以一种使其可以安全进行突变测试的方式编写它,或者通过重构您提供的形式,或者其他一些方式(可能是递归的? )。

你的问题实际上可以归结为:是否存在真实生活情况,我们关心元素0是否包含在循环操作中或从循环操作中排除,并且我们无法围绕该操作编写测试具体方面?我的直觉是拒绝。

你的琐碎例子可能是my blog中缺少我称之为测试驱动的一个例子,写了NinjaTurtles。在你没有重构这个方法的情况下的含义。

答案 1 :(得分:3)

“突变测试失败”的一个自然案例是用于矩阵转置的算法。为了使它更适合单个for循环,请为此任务添加一些约束:让矩阵为非正方形并且需要转置到位。这些约束使得一维阵列最适合存储矩阵,并且可以使用for循环(通常从索引'1'开始)来处理它。如果从索引'0'开始,则没有任何变化,因为矩阵的左上角元素总是转换为自身。

有关此类代码的示例,请参阅answer to other question(不在C#中,抱歉)。

这里“突变一个”测试失败,重构测试不会改变它。我不知道代码本身是否可以重构以避免这种情况。理论上它可能是,但应该太难。


我前面提到的代码片段并不是一个完美的例子。如果for循环被两个嵌套循环替换(就像行和列一样),它仍然可以被重构,然后将这些行和列重新计算回一维索引。它仍然提供了如何制作一些无法重构的算法的想法(尽管不是很有意义)。

按照递增索引的顺序迭代一组正整数,每个索引将其对计算为i + i % a[i],如果它不在边界之外,则交换这些元素:

for (var i = 1; i < a.Length; i++)
{
    var j = i + i % a[i];
    if (j < a.Length)
        Swap(a[i], a[j]);
}

这里再次[0]是“不可移动的”,重构测试不会改变这一点,重构代码本身几乎是不可能的。


另一个“有意义的”例子。让我们实现一个隐式二进制堆。它通常放在某个数组中,从索引'1'开始(与从索引'0'开始相比,这简化了许多二进制堆计算)。现在为此堆实现一个复制方法。此复制方法中的“逐个”问题无法检测,因为索引零未使用且C#对所有数组进行零初始化。这与OP的数组求和类似,但不能重构。

严格地说,你可以重构整个班级并从'0'开始。但是只更改“复制”方法或测试并不能防止“一次性突变”测试失败。二进制堆类可以被视为使用未使用的第一个元素复制数组的动机。

int[] dst = new int[src.Length];
for (var i = 1; i < src.Length; i++)
{
    dst[i] = src[i];
}

答案 2 :(得分:1)

是的,有很多,假设我已经理解了你的问题。

与您的案例相似的是:

public int MultiplyTo(int max)
{
    int product = 1;
    for (var i = 1; i <= max; i++)
    {
        product *= i;
    }
    return product;
}

这里,如果从0开始,结果将为0,但如果从1开始,结果应该是正确的。 (虽然它不会区分1和2之间的区别!)。

答案 3 :(得分:1)

不完全确定你在寻找什么,但在我看来,如果你将sum的初始值从0改为/变为1,你应该通过测试:

public int SumTo(int max) 
{ 
  int sum = 1; // Now we are off-by-one from the beginning!
  for (var i = 0; i <= max; i++) 
  { 
    sum += i; 
  } 
  return sum; 
}

根据评论更新:

当在索引0的处理(或没有索引)的情况下违反循环不变量时,循环将不会在突变后失败。大多数此类特殊情况可以在循环外重构,但考虑1 / x的总和:

for (var i = 1; i <= max; i++) {
  sum += 1/i;
}

这样可以正常工作,但是如果你将初始的bundle从1变为0,那么测试将失败,因为1/0是无效的操作。