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

时间:2011-05-20 21:21:54

标签: ruby unit-testing random statistics probability

我有一些代码可以根据加权随机提供内容。重量更重的东西更有可能随机选择。现在我是一个很好的rubyist,我想用测试来覆盖所有这些代码。而且我想测试根据正确的概率获取的东西。

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

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

  • 获取一个随机项很多次,并将实际比率与预期比率进行比较。但除非我可以无数次运行,否则这将永远不会是完美的,如果我在RNG中运气不好,可能会间歇性地失败。

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

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

6 个答案:

答案 0 :(得分:9)

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

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

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

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

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

答案 1 :(得分:8)

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

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

答案 2 :(得分:3)

如果您想沿着一致的种子路线前进,请查看Kernel#srand

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

引用文档(强调添加):

  

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

答案 3 :(得分:0)

对于测试,使用以下简单的实例Kernel.rand perfectly reasonable LCPRNG:

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

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

答案 4 :(得分:0)

我的建议:将#2和#3结合起来。设置一个随机种子,然后大量运行测试。

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

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

选项2是拥有不依赖内部知识的真正黑匣子解决方案的唯一方法。如果运行足够多次,则随机故障的可能性可以忽略不计。 (您可以挖一个统计老师来帮助您计算“足够高”,或者您可以选择一个非常大的数字。)

但是,如果您过于挑剔并且“微不足道”还不够好,则将#2和#3结合使用将确保一旦测试开始通过,它将继续通过。即使只有微不足道的失败风险,也只有在您触摸被测代码时才会冒出来。只要您不理会代码,就可以保证测试将始终正常运行。

答案 5 :(得分: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)