关闭一个错误和突变测试

本文关键字:错误 突变 测试 一个 | 更新日期: 2023-09-27 18:32:47

在为我最喜欢的突变测试框架(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++(。

错!

第一次运行时变得非常明显,在这种特定情况下它永远不会起作用。 为什么? 因为将代码更改为

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 开始的循环可能导致无法以类似方式重构(测试或测试中的代码(的"突变 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},突变测试将失败。 因此,成功的重构证明了测试。

关闭一个错误和突变测试

我认为使用这种特定方法,有两种选择。你要么承认它不适合突变测试,因为这种数学异常,要么你试图以一种可以安全地进行突变测试的方式编写它,要么通过重构到你给出的形式,要么通过其他方式(可能是递归的?(。

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

你的琐碎例子可能是缺乏我在博客中所说的测试驱动的例子,写的是忍者神龟。这意味着如果您尚未尽可能重构此方法。

"突变测试失败"的一个自然情况是矩阵转置算法。为了使它更适合单个 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'开始一切。但是仅更改"复制"方法或测试并不能防止"突变一"测试失败。二进制堆类可以被视为复制具有未使用的第一个元素的数组的动机。

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

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

与您的情况类似的是:

public int MultiplyTo(int max)
{
    int product = 1;
    for (var i = 1; i <= max; i++)
    {
        product *= i;
    }
    return product;
}
在这里,如果它从 0 开始,结果

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

不太确定您到底在寻找什么,但在我看来,如果您将总和的初始值从 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;
}

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