具有返回随机结果的函数的单元测试


70

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

我有一个函数,它返回一定范围内的随机日期。我输入了一个日期,返回日期始终在给定日期之前的1到40年之间。

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

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


考虑使用类似QuickCheck的方法为随机函数生成输入,然后验证输出是否与某些属性匹配。

Answers:


33

除了测试该函数返回的日期在期望的范围内之外,您还希望确保结果分布合理。您描述的测试将通过一个仅返回您发送日期的函数!

因此,除了多次调用该函数并测试结果是否保持在所需范围内之外,我还尝试评估分布,也许是将结果放入存储桶中,并检查存储桶中的结果数量大致相等完成。您可能需要进行100多次调用才能获得稳定的结果,但这听起来不像是昂贵的(运行时)函数,因此您可以轻松地将其运行几K次迭代。

以前,我使用非均匀的“随机”功能时遇到了问题。它们可能是真正的痛苦,值得尽早进行测试。


1
实际上-有针对特殊分布进行测试的统计测试(例如,Pearson的卡方检验)。它们在有限的范围内以比Bill所提及的更少的价值运作。由于这是一项统计测试,因此该测试有时可能会失败(错误否定)。
Tobias Langner 2009年

2
不同意这个答案-您正在有效地测试随机数生成器,也就是“其他人的代码”。伪造发电机(如另一个答案中所述)是正确的方法。您的测试应仅检查是否调用了随机生成器,并且其结果是否按预期处理。
克里斯·孔雀

57

模拟或伪造随机数生成器

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

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);
    }
}

在您的测试中,只需伪造或模拟IRandomGenerator以返回罐头食品。


1
顺便说一句,许多语言都有模拟框架,您可以使用它们来简化模拟。功能更强大的功能(例如PowerMock)甚至可以允许对RNG的重写调用,而无需依赖注入。
2014年

+1表示DI,实际上可以进行单元测试。单元测试应该是快速,独立的,并且总是总是无故障地返回相同的结果,而不管一天中的时间,执行顺序等等。题为“大多数时候应该给出与该结果相似的东西”的测试不是可以信任的单元测试。当然,在这种情况下,它实际上根本不会失败,但是您需要100%信任您的测试,否则它们将毫无用处。
2015年

9

我认为您要测试此问题的三个不同方面。

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

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

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

前两件事不是我要构建到单元测试套件中的东西。我在设计系统时就证明了它们。如daniel.rikowski建议的那样,我可能会编写一个测试工具来生成大量日期并执行卡方检验来完成此任务。我还要确保直到测试边缘都处理完之后,这个测试工具才终止(假设我的随机数范围足够小,我可以避免这种情况)。我已对此进行了记录,以便任何尝试改进算法的人都知道这是一个重大变化。

最后一个我要进行单元测试的东西。我需要知道,没有什么代码可以破坏该算法的实现。发生这种情况时,我得到的第一个迹象是测试将失败。然后,我将返回代码,发现其他人认为他们正在修复某些东西,而是破坏了它。如果有人确实修复了该算法,那也将由他们来修复此测试。


8

您无需控制系统即可确定结果。您采用的是正确的方法:确定对于函数输出重要的内容并对此进行测试。在这种情况下,结果必须在40天之内是很重要的,您正在对此进行测试。同样重要的是,它不一定总是返回相同的结果,因此也要对此进行测试。如果您想变得更聪明,可以测试结果是否通过某种随机性测试。


5

通常,我完全使用您建议的方法:控制随机数生成器。使用默认种子将其初始化以进行测试(或将其替换为返回适合我的测试用例的数字的代理),因此我具有确定性/可测试的行为。



2

根据您的函数创建随机日期的方式,您可能还需要检查非法日期:可能的or年或30天月份的第31天。


2

未表现出确定性行为的方法无法正确地进行单元测试,因为结果在一次执行之间会有所不同。解决此问题的一种方法是为单元测试播种具有固定值的随机数生成器。您还可以提取日期生成类的随机性(并因此应用“单一职责原则”),并为单元测试注入已知值。


2

当然,使用固定种子随机数生成器会很好用,但是即使那样,您也只是在尝试测试无法预测的值。没关系 这等效于进行一堆固定测试。但是,请记住-测试重要的东西,但不要尝试测试所有内容。我相信随机测试是一种尝试测试所有内容的方法,并且效率不高(或快速)。在发现错误之前,您可能必须进行大量随机测试。

我要在这里说的是,您只需要为系统中发现的每个错误编写一个测试。您可以测试边缘情况,以确保您的功能即使在极端条件下也可以运行,但实际上,这是您最好的选择,而无需花费太多时间或使单元测试运行缓慢,或者只是浪费处理器周期。


1

我建议重写随机函数。我正在用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。

测试随机函数的真正随机性是一个单独的测试。我会避免在单元测试中测试随机性,而会进行单独的测试,并使用您正在使用的编程语言来搜索随机函数的真实随机性。非确定性测试(如果有的话)应排除在单元测试之外。也许有一个单独的套件用于那些测试,这需要人工输入或更长的运行时间,以最大程度地减少真正通过的失败机会。


0

我不认为单元测试是为此目的而设计的。您可以对返回随机值的函数使用单元测试,但可以使用固定种子,在这种情况下,它们不是随机的,也就是说,对于随机种子,我认为单元测试不是您想要的,例如对于RNG,您的意思是进行系统测试,在该测试中,您需要多次运行RNG,并查看其分布或时刻。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.