8

在为我最喜欢的突变测试框架(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 个(the 0、the1和 the ++)。

错误的!

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

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 都会导致错误。(鉴于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

4 回答 4

4

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

您的问题实际上归结为:在现实生活中,我们是否关心元素 0 是否包含在循环操作中或排除在循环操作中,并且我们无法围绕该特定方面编写测试?我的直觉是说不。

您的微不足道的例子可能是我在我的博客中提到的缺乏测试驱动的例子,关于NinjaTurtles的文章。这意味着您没有尽可能地重构此方法。

于 2012-04-30T13:19:22.033 回答
3

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

有关此类代码的示例,请参阅其他问题的答案(不在 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]);
}

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


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

严格来说,你可以重构整个类,一切从“0”开始。但是仅更改“复制”方法或测试并不能防止“突变关闭”测试失败。Binary Heap 类可以被视为复制具有未使用的第一个元素的数组的动机。

int[] dst = new int[src.Length];
for (var i = 1; i < src.Length; i++)
{
    dst[i] = src[i];
}
于 2012-05-03T13:03:16.697 回答
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;
}

这可以正常工作,但是如果您将初始边界从 1 更改为 0,则测试将失败,因为 1/0 是无效操作。

于 2012-04-30T12:21:08.223 回答
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!)。

于 2012-04-30T11:54:27.700 回答