【问题标题】:Ruby: Using rand() in code but writing tests to verify probabilitiesRuby:在代码中使用 rand() 但编写测试来验证概率
【发布时间】:2019-04-26 06:15:29
【问题描述】:

我有一些代码可以根据加权随机提供内容。更重的东西更有可能被随机选择。现在作为一名优秀的 ruby​​ist,我当然想用测试来覆盖所有这些代码。而且我想测试是否根据正确的概率获取东西。

那么我该如何测试呢?为应该是随机的东西创建测试使得比较实际与预期变得非常困难。我有一些想法,以及为什么它们不能很好地工作:

  • Stub Kernel.rand 在我的测试中返回固定值。这很酷,但是 rand() 被多次调用,我不确定我是否可以通过足够的控制来测试我需要的东西。

  • 大量获取随机项目并将实际比率与预期比率进行比较。但除非我可以无限次运行它,否则它永远不会完美,如果我在 RNG 中运气不好,可能会间歇性地失败。

  • 使用一致的随机种子。这使得 RNG 可重复,但它仍然没有给我任何验证,即项目 A 将在 80% 的时间发生(例如)。

那么我可以使用什么样的方法来编写随机概率的测试覆盖率?

【问题讨论】:

  • 您能否详细说明最后的反对意见?为什么你不能用种子 PRNG 测试你的分布曲线?
  • @DigitalRoss 因为它仍然是随机的。因此,如果我得到我想要的东西,10 次中有 3 次实际上并不能说明我是否满足 25% 的概率。提前知道随机值并没有真正的帮助。

标签: ruby unit-testing random statistics probability


【解决方案1】:

我认为你应该分开你的目标。一种是像您提到的那样对 Kernel.rand 进行存根。例如,使用 rspec,您可以执行以下操作:

test_values = [1, 2, 3]
Kernel.stub!(:rand).and_return( *test_values )

请注意,除非您使用内核作为接收方调用 rand,否则此存根将不起作用。如果你只是调用“rand”,那么当前的“self”会收到消息,你实际上会得到一个随机数而不是 test_values。

第二个目标是做一些类似现场测试的事情,你可以在其中实际生成随机数。然后,您将使用某种容差来确保您接近所需的百分比。然而,这永远不会是完美的,并且可能需要一个人来评估结果。但是这样做仍然很有用,因为您可能会意识到另一个随机数生成器可能会更好,例如从 /dev/random 读取。此外,进行这种测试也很好,因为假设您决定迁移到一种新的平台,该平台的系统库不擅长生成随机性,或者某个版本存在一些错误。该测试可能是一个警告信号。

这真的取决于你的目标。你只想测试你的加权算法,还是随机性?

【讨论】:

  • 这是我缺少的认知部分。对 rand 存根使用多个类似的返回值让我得到了我需要的东西。我现在可以测试可预测且完全均匀的值分布,并确保它们做正确的事情。谢谢!
【解决方案2】:

最好存根 Kernel.rand 以返回固定值。

Kernel.rand 不是您的代码。您应该假设它有效,而不是尝试编写测试来测试它而不是您的代码。并且使用您选择并明确编码的一组固定值比添加对 rand 为特定种子生成的内容的依赖项要好。

【讨论】:

    【解决方案3】:

    如果你想走一致的种子路线,请查看Kernel#srand

    http://www.ruby-doc.org/core/classes/Kernel.html#M001387

    引用文档(已添加重点):

    播种伪随机数 生成器到 number 的值。如果 数字被省略或为零,种子 发电机使用的组合 时间、进程 ID 和序列 数字。 (如果这也是行为 Kernel::rand 没有被调用 以前调用 srand,但没有 序列。)通过设置种子 到一个已知的值,可以制作脚本 在测试期间具有确定性。 返回之前的种子值。还 请参阅 Kernel::rand。

    【讨论】:

      【解决方案4】:

      为了测试,使用以下简单但 perfectly reasonable LCPRNG 存根 Kernel.rand:

      @@q = 0
      def r
        @@q = 1_103_515_245 * @@q + 12_345 & 0xffff_ffff
        (@@q >> 2) / 0x3fff_ffff.to_f
      end
      

      如果您的代码兼容,您可能希望跳过除法并直接使用整数结果,因为结果的所有位都将是可重复的,而不仅仅是“大多数”。这将您的测试与 Kernel.rand 的“改进”隔离开来,并且应该允许您测试您的分布曲线。

      【讨论】:

      • 你简直让我大吃一惊……但我无法弄清楚这是如何产生随机性的。
      【解决方案5】:

      我的建议:结合#2 和#3。设置一个随机种子,然后多次运行测试。

      我不喜欢 #1,因为这意味着您的测试与您的实现紧密耦合。如果您更改使用 rand() 输出的方式,即使结果正确,测试也会中断。单元测试的重点是您可以重构方法并依靠测试来验证它是否仍然有效。

      选项#3 本身与#1 有同样的问题。如果你改变你使用 rand() 的方式,你会得到不同的结果。

      选项 #2 是获得真正黑盒解决方案的唯一方法,该解决方案不依赖于了解您的内部情况。如果你运行它的次数足够多,随机失败的机会可以忽略不计。 (你可以找一个统计老师帮你计算“足够高”,或者你可以选择一个非常大的数字。)

      但是,如果您过于挑剔并且“可以忽略不计”还不够好,那么 #2 和 #3 的组合将确保一旦测试开始通过,它将继续通过。即使是可以忽略不计的失败风险也只会在您接触被测代码时才会出现;只要您不理会代码,就可以保证测试始终正确运行。

      【讨论】:

        【解决方案6】:

        当我需要从随机数派生的东西中获得可预测的结果时,我通常希望控制 RNG,这意味着最简单的方法是使其可注入。尽管可以覆盖/存根 rand,但 Ruby 提供了一种很好的方法来将您的代码传递给带有某些值的 RNG:

        def compute_random_based_value(input_value, random: Random.new)
           # ....
        end
        

        然后注入一个我在测试中当场制作的随机对象,带有一个已知的种子:

        rng = Random.new(782199) # Scientific dice roll
        compute_random_based_value(your_input, random: rng)
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2023-04-04
          • 2013-02-24
          • 2021-08-24
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2021-10-04
          • 1970-01-01
          相关资源
          最近更新 更多