简而言之,我当前的项目涉及“约束随机事件”的创建。我基本上是在制定检查时间表。其中一些是基于严格的计划约束;您每周星期五10:00 AM进行一次检查。其他检查是“随机的”;有一些基本的可配置要求,例如“每周必须进行3次检查”,“必须在9 AM-9PM的时间之间进行检查”以及“在同一8小时内不应进行两次检查”,但是在为一组特定的检查配置的任何限制内,得出的日期和时间均不可预测。
单元测试和TDD,IMO在此系统中具有巨大的价值,因为它们可以用于按增量方式构建它,而整套需求仍然不完整,并确保我不会“过度设计”它来做我不喜欢的事情目前不知道我需要。严格的时间表对TDD来说是小菜一碟。但是,当我为系统的随机部分编写测试时,我发现很难真正定义要测试的内容。我可以断言调度程序产生的所有时间都必须在约束范围内,但是我可以实现通过所有此类测试的算法,而实际时间却不是很“随机”。实际上,这正是发生的事情。我发现了一个问题,尽管时间无法精确预测,但它属于允许的日期/时间范围的一小部分。该算法仍然通过了我认为我可以合理做出的所有断言,并且我无法设计在这种情况下会失败的自动测试,但是在给出“更多随机”结果时通过。我必须证明该问题是通过重组一些现有测试以重复多次来解决的,并目视检查生成的时间是否在整个允许范围内。
有没有人提供设计非预期行为的提示?
感谢所有的建议。主要观点似乎是,我需要进行确定性测试才能获得确定性,可重复性和可肯定的结果。说得通。
我创建了一组“沙盒”测试,其中包含用于约束过程(可能为任意长的字节数组在最小值和最大值之间变长的过程)的候选算法。然后,我通过一个FOR循环运行该代码,该循环为该算法提供了几个已知的字节数组(刚开始时从1到10,000,000的值),并使该算法将每个值限制为1009到7919之间的值(我使用质数来确保算法不会在输入和输出范围之间经过一些偶然的GCF)。计算得到的约束值,并生成直方图。要“通过”,所有输入都必须反映在直方图中(为了确保我们不会“丢失”任何值),直方图中任意两个存储桶之间的差不能大于2(实际上应小于等于1) ,但请继续关注)。获胜的算法(如果有)可以直接剪切并粘贴到生产代码中,并进行永久测试以进行回归。
这是代码:
private void TestConstraintAlgorithm(int min, int max, Func<byte[], long, long, long> constraintAlgorithm)
{
var histogram = new int[max-min+1];
for (int i = 1; i <= 10000000; i++)
{
//This is the stand-in for the PRNG; produces a known byte array
var buffer = BitConverter.GetBytes((long)i);
long result = constraintAlgorithm(buffer, min, max);
histogram[result - min]++;
}
var minCount = -1;
var maxCount = -1;
var total = 0;
for (int i = 0; i < histogram.Length; i++)
{
Console.WriteLine("{0}: {1}".FormatWith(i + min, histogram[i]));
if (minCount == -1 || minCount > histogram[i])
minCount = histogram[i];
if (maxCount == -1 || maxCount < histogram[i])
maxCount = histogram[i];
total += histogram[i];
}
Assert.AreEqual(10000000, total);
Assert.LessOrEqual(maxCount - minCount, 2);
}
[Test, Explicit("sandbox, does not test production code")]
public void TestRandomizerDistributionMSBRejection()
{
TestConstraintAlgorithm(1009, 7919, ConstrainByMSBRejection);
}
private long ConstrainByMSBRejection(byte[] buffer, long min, long max)
{
//Strip the sign bit (if any) off the most significant byte, before converting to long
buffer[buffer.Length-1] &= 0x7f;
var orig = BitConverter.ToInt64(buffer, 0);
var result = orig;
//Apply a bitmask to the value, removing the MSB on each loop until it falls in the range.
var mask = long.MaxValue;
while (result > max - min)
{
mask >>= 1;
result &= mask;
}
result += min;
return result;
}
[Test, Explicit("sandbox, does not test production code")]
public void TestRandomizerDistributionLSBRejection()
{
TestConstraintAlgorithm(1009, 7919, ConstrainByLSBRejection);
}
private long ConstrainByLSBRejection(byte[] buffer, long min, long max)
{
//Strip the sign bit (if any) off the most significant byte, before converting to long
buffer[buffer.Length - 1] &= 0x7f;
var orig = BitConverter.ToInt64(buffer, 0);
var result = orig;
//Bit-shift the number 1 place to the right until it falls within the range
while (result > max - min)
result >>= 1;
result += min;
return result;
}
[Test, Explicit("sandbox, does not test production code")]
public void TestRandomizerDistributionModulus()
{
TestConstraintAlgorithm(1009, 7919, ConstrainByModulo);
}
private long ConstrainByModulo(byte[] buffer, long min, long max)
{
buffer[buffer.Length - 1] &= 0x7f;
var result = BitConverter.ToInt64(buffer, 0);
//Modulo divide the value by the range to produce a value that falls within it.
result %= max - min + 1;
result += min;
return result;
}
...结果如下:
LSB拒绝(将数字移位直到其在该范围内)是很麻烦的,原因很容易解释。当您将任何数字除以2直到小于最大值时,您会立即退出,并且对于任何非平凡的范围,都会使结果偏向上三分之一(如直方图的详细结果所示) )。这正是我从完成日期中看到的行为。所有时间都是在特定日子的下午。
MSB拒绝(将最高有效位一次从最高位删除直到它在范围内)会更好,但是又一次,因为您要砍掉每一位很大的数字,所以它的分布不均。您不太可能在高端和低端获得数字,因此您会偏向中间三分之一。这可能有益于希望将随机数据“规范化”为钟形曲线的人,但是两个或多个较小的随机数之和(类似于掷骰子)将为您提供更自然的曲线。就我而言,它失败了。
唯一通过此测试的方法是通过模除约束,这也是三者中最快的。根据定义,Modulo将根据可用输入产生尽可能均匀的分布。