Ruby:在代码中使用rand(),但编写测试以验证概率

7
我有一些代码,根据加权随机分配物品。权重更高的物品更有可能被随机选择。作为一名优秀的ruby程序员,我当然希望用测试覆盖所有这些代码。我想测试是否按照正确的概率获取物品。
那么我该如何进行测试呢?对于应该是随机的东西创建测试,很难比较实际结果和预期结果。以下是我提出的一些想法以及它们不太可行的原因:
1. 在我的测试中打桩 Kernel.rand,返回固定值。这很酷,但 rand() 会被调用多次,我不确定我能够通过足够的控制来测试我需要的内容。
2. 获取一个随机项大量次数,并比较实际比例和预期比例。但除非我可以无限次运行,否则这永远不会完美,并且如果 RNG 运气不好,可能会间歇性失败。
3. 使用一致的随机种子。这使 RNG 可重复,但仍然不能为我提供任何验证,例如 A 物品将发生80% 的时间。
那么什么样的方法可以用来编写测试覆盖随机概率呢?

你能详细说明最后一个反对意见吗?为什么不能使用种子 PRNG 来测试分布曲线? - DigitalRoss
@DigitalRoss 因为它仍然是随机的。所以,如果我在10次尝试中3次得到我想要的东西,实际上并不能告诉我是否满足了25%的概率。提前知道随机值并没有真正帮助。 - Alex Wayne
6个回答

9

我认为你应该分开你的目标。一个是像你提到的那样存根Kernel.rand。例如,使用rspec,你可以这样做:

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

请注意,除非您将Kernel作为接收器调用rand,否则此存根将无法工作。如果您只是调用“rand”,那么当前的“self”将接收消息,您实际上会得到一个随机数,而不是测试值。
第二个目标类似于进行现场测试,您需要生成随机数字。然后使用某种容差来确保您接近所需的百分比。但这永远不会完美,可能需要人工评估结果。但是仍然很有用,因为您可能会意识到另一个随机数生成器可能更好,例如从/dev/random读取。此类测试也很有用,因为假设您决定迁移到新的平台类型,其系统库在生成随机性方面不如以前的版本,或者某个版本存在一些错误。测试可以成为警告信号。
这真的取决于您的目标。您只想测试加权算法还是随机性?

这就是我缺失的认知部分。像那样使用多个返回值来获取rand存根,让我得到了我所需要的东西。现在我可以测试可预测和完全均匀的值分布,并确保它们做正确的事情。谢谢! - Alex Wayne

8

最好将Kernel.rand桩设为返回固定值。

Kernel.rand不是你的代码。你应该假设它能正常工作,而不是试图编写测试来测试它而不是你的代码。使用你选择并明确编码的一组固定值比添加对特定种子生成的rand产生的依赖性更好。


3
如果你想走一条稳定的种子路线,可以查看Kernel#srand

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

引用文档(重点添加):

将伪随机数生成器的种子设置为数字的值。如果省略数字或将其设置为零,则使用时间、进程ID和序列号的组合来初始化生成器。(如果在未先调用srand的情况下调用Kernel::rand,但没有序列,则也是这种行为)。通过将种子设置为已知值,可以使脚本在测试期间变得确定性。返回以前的种子值。另请参阅Kernel::rand。


0
通常情况下,当我需要从随机数派生出可预测的结果时,我通常希望控制随机数生成器(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)

0

为了测试,请使用以下简单但完全合理的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的“改进”隔离开来,并应允许您测试分布曲线。

你刚刚让我大开眼界了...但我还是想不明白这个如何产生随机性。 - Michael

0
我的建议是:将#2和#3结合起来。设置一个随机种子,然后运行大量的测试。
我不喜欢#1,因为它意味着你的测试与你的实现紧密耦合。如果你改变了如何使用rand()的输出,即使结果是正确的,测试也会失败。单元测试的目的是让你重构方法并依赖于测试来验证它仍然有效。
选项#3本身具有与#1相同的问题。如果你改变了如何使用rand(),你将得到不同的结果。
选项#2是唯一的方式,可以拥有真正的黑盒解决方案,而不依赖于知道你的内部情况。如果你运行足够多次,随机失败的几率是可以忽略不计的。(你可以找一个统计老师来帮助你计算“足够高”,或者你可以选择一个非常大的数字。)
但是,如果你非常挑剔,“可忽略不计”还不够好,那么#2和#3的组合将确保一旦测试开始通过,它将继续通过。即使在测试代码下进行修改时,只有那个微小的失败风险才会出现;只要你不动代码,你就可以保证测试始终正确地工作。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接