【问题标题】:Unit Testing with functions that return random results使用返回随机结果的函数进行单元测试
【发布时间】:2010-09-23 15:10:24
【问题描述】:

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

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

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

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

【问题讨论】:

  • 考虑使用 QuickCheck 之类的工具来生成随机函数的输入,然后验证输出是否与某些属性匹配。

标签: c# .net unit-testing xunit.net


【解决方案1】:

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

【讨论】:

    【解决方案2】:

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

    【讨论】:

      【解决方案3】:

      除了测试函数返回所需范围内的日期外,您还希望确保结果分布良好。您描述的测试将通过一个简单地返回您发送日期的函数!

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

      我以前遇到过不统一的“随机”函数的问题。它们可能真的很痛苦,值得尽早测试。

      【讨论】:

      • 实际上 - 有针对特殊分布的统计检验(例如 Pearson 的卡方检验)。它们在一定范围内工作,价值比比尔提到的要少。由于这是一项统计测试,因此测试可能会时不时地失败(假阴性)。
      • 不同意这个答案 - 您正在有效地测试随机数生成器,也就是“别人的代码”。如另一个答案中所述伪造生成器是正确的方法。您的测试应该只检查随机生成器是否被调用并且其结果是否按预期处理。
      【解决方案4】:

      如果您想检查随机数的质量(在独立性方面),有几种方法可以做到这一点。一种好方法是Chi square test

      【讨论】:

        【解决方案5】:

        模拟或伪造随机数生成器

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

        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 以返回罐头。

        【讨论】:

        • 顺便说一句,许多语言都有模拟框架,您可以使用它们来简化模拟。更强大的(例如,PowerMock)甚至可以允许覆盖对 RNG 的调用,而无需依赖注入。
        • +1 表示 DI 和实际使单元测试成为可能。单元测试应该是快速的、独立的并且总是总是总是返回相同的结果,不管一天中的时间、执行顺序等等。一个说“应该在大多数情况下给出类似于这个结果的东西”的测试不是一个可以信任的单元测试。诚然,在这种情况下,它几乎不会真的失败,但你需要 100% 相信你的测试,否则它们就没什么用了。
        【解决方案6】:

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

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

        第二个:算法是否正确处理边缘情况?也就是说,当随机数生成器产生最高或最低允许值时,是否会出现任何中断?

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

        前两件事不是我要构建到单元测试套件中的东西。在设计系统时,我会证明它们。正如 daniel.rikowski 建议的那样,我可能会通过编写一个生成无数日期并执行卡方检验的测试工具来做到这一点。我还要确保这个测试工具在它处理两种边缘情况之前不会终止(假设我的随机数范围足够小,我可以摆脱这个)。我会记录这一点,以便任何前来尝试改进算法的人都知道这是一个突破性的变化。

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

        【讨论】:

          【解决方案7】:

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

          【讨论】:

            【解决方案8】:

            不表现出确定性行为的方法无法正确进行单元测试,因为结果会因一次执行而异。解决此问题的一种方法是seed 具有固定值的随机数生成器用于单元测试。您还可以提取日期生成类的随机性(从而应用Single Responsibility Principle),并为单元测试注入已知值。

            【讨论】:

              【解决方案9】:

              当然,使用固定种子随机数生成器可以正常工作,但即便如此,您也只是在尝试测试您无法预测的内容。没关系。这相当于有一堆固定的测试。但是,请记住——测试什么是重要的,但不要尝试测试所有内容。我相信随机测试是一种尝试测试所有内容的方法,它效率不高(或速度不快)。在遇到错误之前,您可能需要运行大量随机测试。

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

              【讨论】:

                【解决方案10】:

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

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

                【讨论】:

                  【解决方案11】:

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

                  【讨论】:

                    猜你喜欢
                    • 2011-02-06
                    • 2012-07-18
                    • 2013-10-09
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 2020-09-28
                    • 1970-01-01
                    • 1970-01-01
                    相关资源
                    最近更新 更多