使用返回随机结果的函数进行单元测试

我不认为这是特定于某种语言或框架的,但我使用的是 xUnit.net 和 C # 。

我有一个函数,它返回一个特定范围内的随机日期。我传递一个日期,返回日期总是在给定日期之前的1到40年的范围内。

现在我只想知道是否有一个好的方法来单元测试这个。最好的方法似乎是创建一个循环,让函数运行100次,并断言这100个结果中的每一个都在期望的范围内,这就是我目前的方法。

我还意识到,除非我能够控制我的随机生成器,否则不会有一个完美的解决方案(毕竟,结果是随机的) ,但我想知道,当你必须测试功能,返回一个随机的结果在一定范围内,你采取什么方法?

31961 次浏览

通常我会使用您建议的方法: 控制随机生成器。 用默认的种子初始化它以进行测试(或者用代理返回适合我的测试用例的数字来替换它) ,这样我就有了确定性/可测试的行为。

您不需要控制系统来使结果具有确定性。您采用的方法是正确的: 确定函数输出的重要性并对其进行测试。在这种情况下,结果在40天的范围内是非常重要的,并且您正在对此进行测试。同样重要的是,它并不总是返回相同的结果,所以也测试一下。如果你想更有想象力,你可以测试结果是否通过某种随机性测试。.

除了测试函数是否返回所需范围内的日期之外,您还需要确保结果是分布良好的。您描述的测试将通过一个函数,该函数只返回您发送的日期!

因此,除了多次调用函数并测试结果是否保持在期望的范围之外,我还会尝试评估分布情况,也许可以将结果放入桶中,并在完成后检查桶中的结果数目大致相同。您可能需要超过100个调用来获得稳定的结果,但是这听起来不像是一个昂贵的(运行时智能的)函数,因此您可以轻松地运行它几个 K 迭代。

我以前遇到过一个非统一的“随机”函数的问题. . 它们可能是一个真正的痛苦,值得早期进行测试。

如果您想检查随机数的质量(就独立性而言) ,有几种方法可以做到这一点。一个好的方法是 卡方检验

模拟或伪造随机数生成器

做这样的事情... 我没有编译它,所以可能会有一些语法错误。

public interface IRandomGenerator
{
double Generate(double max);
}


public class SomethingThatUsesRandom
{
private readonly IRandomGenerator _generator;


private class DefaultRandom : IRandomGenerator
{
public double Generate(double max)
{
return (new Random()).Next(max);
}
}


public SomethingThatUsesRandom(IRandomGenerator generator)
{
_generator = generator;
}


public SomethingThatUsesRandom() : this(new DefaultRandom())
{}


public double MethodThatUsesRandom()
{
return _generator.Generate(40.0);
}
}

在您的测试中,只需伪造或模拟 IRRandom Generator 来返回一些罐装的东西。

我认为这个问题有三个不同的方面需要测试。

第一个问题: 我的算法是正确的吗?也就是说,给定一个正常运行的随机数生成器,它会生成随机分布在范围内的日期吗?

第二个问题: 算法能否正确处理边缘情况?也就是说,当随机数生成器产生允许的最高值或最低值时,有什么会中断吗?

第三个问题: 我的算法实现是否有效?也就是说,给定一个已知的伪随机输入列表,它是否会产生预期的伪随机日期列表?

前两件事情我不会构建到单元测试套件中。我会在设计系统的时候证明这一点。我可能会编写一个测试工具,生成无数个日期,并按 Daniel.rikowski 的建议执行卡方测试。我还要确保这个测试工具不会终止,直到它处理了两种边缘情况(假设我的随机数范围足够小,我可以摆脱这种情况)。我会记录下来,这样任何想要改进算法的人都会知道这是一个重大的改变。

最后一个 的东西,我会做一个单元测试。我需要知道没有任何东西潜入代码破坏了这个算法的实现。当这种情况发生时,我得到的第一个信号就是测试会失败。然后我会回到代码中,发现有人以为他们在修理什么东西,结果却把它弄坏了。如果有人 是的修复了算法,那么修复这个测试也是他们的责任。

根据您的函数如何创建随机日期,您可能还想检查非法日期: 不可能的闰年,或30天月份的第31天。

没有显示 确定性行为的方法不能进行适当的单元测试,因为结果在不同的执行中会有所不同。解决这个问题的一个方法是使用 种子随机数生成器,该生成器为单元测试提供一个固定的值。您还可以提取日期生成类的随机性(从而应用 单一责任原则) ,并为单元测试注入已知的值。

当然,使用一个固定的种子随机数生成器将工作得很好,但即使那样,您也只是试图测试那些您无法预测的东西。没关系。这相当于有一堆固定的测试。然而,请记住——测试什么是重要的,但不要试图测试所有的东西。我相信随机测试是一种试图测试所有东西的方法,但它并不高效(或快速)。在遇到 bug 之前,您可能必须进行大量的随机测试。

我在这里想说的是,您应该简单地为系统中发现的每个 bug 编写一个测试。测试边界情况以确保函数在极端条件下也能正常运行,但实际上这是最好的方法,而不需要花费太多时间,或者让单元测试运行缓慢,或者只是浪费处理器周期。

我建议重写这个随机函数。我是用 PHP 进行单元测试的,所以我写了这段代码:

// If we are unit testing, then...
if (defined('UNIT_TESTING') && UNIT_TESTING)
{
// ...make our my_rand() function deterministic to aid testing.
function my_rand($min, $max)
{
return $GLOBALS['random_table'][$min][$max];
}
}
else
{
// ...else make our my_rand() function truly random.
function my_rand($min = 0, $max = PHP_INT_MAX)
{
if ($max === PHP_INT_MAX)
{
$max = getrandmax();
}
return rand($min, $max);
}
}

然后根据每个测试的需要设置 Random _ table。

测试一个随机函数的真正随机性是一个完全独立的测试。我会避免在单元测试中测试随机性,而是进行单独的测试,并在你使用的编程语言中搜索随机函数的真正随机性。不确定性测试(如果有的话)应该被排除在单元测试之外。也许有一个单独的测试套件,这需要人工输入或更长的运行时间,以尽量减少失败的可能性,这实际上是一个通过。

我不认为单元测试是用来做这个的。您可以对返回随机值的函数使用单元测试,但是使用固定的种子,在这种情况下,它们不是随机的,可以这样说, 对于随机种子,我不认为单元测试是你想要的,例如对于 RNG,你想要的是一个系统测试,在系统测试中,你多次运行 RNG 并查看它的分布或时刻。