我有一些代码可以根据加权随机来提供东西。重量更重的东西更有可能随机选择。现在我是一个很好的rubyist,我想用测试来覆盖所有这些代码。而且我想测试根据正确的概率获取的东西。
那么我该如何测试呢?为随机的东西创建测试会使实际与预期的比较变得非常困难。我有一些想法,以及为什么它们不会很好用:
那么我可以使用什么样的方法来编写随机概率的测试覆盖率?
我认为你应该分开你的目标。一个是你提到的存根Kernel.rand。以rspec为例,你可以这样做:
test_values = [1, 2, 3]
Kernel.stub!(:rand).and_return( *test_values )
请注意,除非您使用Kernel作为接收器调用rand,否则此存根将不起作用。如果你只是调用“rand”,那么当前的“self”将收到消息,你实际上会得到一个随机数而不是test_values。
第二个目标是执行类似于实际生成随机数的字段测试。然后,您可以使用某种公差来确保接近所需的百分比。这永远不会是完美的,并且可能需要人来评估结果。但它仍然有用,因为您可能会意识到另一个随机数生成器可能更好,例如从/ dev / random读取。此外,进行这种测试是很好的,因为假设您决定迁移到一种新的平台,其系统库不太擅长产生随机性,或者某个版本中存在一些错误。测试可能是一个警告标志。
这真的取决于你的目标。你只想测试你的加权算法,还是随机性?
最好是将Kernel.rand存根以返回固定值。
Kernel.rand不是你的代码。您应该假设它有效,而不是尝试编写测试它而不是代码的测试。使用您选择并明确编码的固定值集合比添加对特定种子生成的rand的依赖性更好。
如果你想沿着一致的种子路线走下去,看看Kernel#srand
:
http://www.ruby-doc.org/core/classes/Kernel.html#M001387
引用文档(重点补充):
将伪随机数生成器播种到number的值。如果省略number或为零,则使用时间,进程ID和序列号的组合来生成生成器。 (如果在没有先前调用srand但没有序列的情况下调用Kernel :: rand,这也是行为。)通过将种子设置为已知值,可以在测试期间使脚本成为确定性的。返回先前的种子值。另见Kernel :: rand。
为了测试,使用以下简单但是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,并允许您测试您的分布曲线。
我的建议是:结合#2和#3。设置一个随机种子,然后运行你的测试很多次。
我不喜欢#1,因为这意味着你的测试与你的实现紧密耦合。如果更改了使用rand()输出的方式,即使结果正确,测试也会中断。单元测试的重点在于您可以重构该方法并依赖测试来验证它是否仍然有效。
选项#3本身与#1具有相同的问题。如果你改变使用rand()的方式,你会得到不同的结果。
选项#2是拥有真正黑盒解决方案的唯一方法,它不依赖于了解您的内部结构。如果运行次数足够多,则随机失败的几率可以忽略不计。 (你可以挖掘统计老师来帮助你计算“足够高”,或者你可以选择一个非常大的数字。)
但是如果你挑剔而且“微不足道”还不够好,那么#2和#3的组合将确保一旦测试开始通过,它将继续通过。即使是那种可以忽略不计的失败风险,只有当你触摸被测代码时才会出现;只要您单独保留代码,就可以保证测试始终正常。
很多时候,当我需要从随机数得出的东西可预测的结果时,我通常想要控制RNG,这意味着最简单的方法是使其可注射。虽然可以完成覆盖/存根rand
,但是Ruby提供了一种很好的方法来传递代码,这是一个带有一些值的RNG:
def compute_random_based_value(input_value, random: Random.new)
# ....
end
然后使用已知种子在测试中当场注入一个Random对象:
rng = Random.new(782199) # Scientific dice roll
compute_random_based_value(your_input, random: rng)