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

发布于 2024-11-08 07:48:54 字数 472 浏览 3 评论 0原文

我有一些代码可以根据加权随机提供东西。权重较大的事物更有可能被随机选择。现在,作为一名优秀的 Ruby 专家,我当然希望通过测试来覆盖所有这些代码。我想测试是否按照正确的概率获取内容。

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

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

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

  • 使用一致的随机种子。这使得 RNG 可重复,但它仍然无法验证项目 A 在 80% 的情况下会发生(例如)。

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

I have some code which delivers things based on weighted random. Things with more weight are more likely to be randomly chosen. Now being a good rubyist I of couse want to cover all this code with tests. And I want to test that things are getting fetched according the correct probabilities.

So how do I test this? Creating tests for something that should be random make it very hard to compare actual vs expected. A few ideas I have, and why they wont work great:

  • Stub Kernel.rand in my tests to return fixed values. This is cool, but rand() gets called multiple times and I'm not sure I can rig this with enough control to test what I need to.

  • Fetch a random item a HUGE number of times and compare the actual ratio vs the expected ratio. But unless I can run it an infinite number of times, this will never be perfect and could intermittently fail if I get some bad luck in the RNG.

  • Use a consistent random seed. This makes the RNG repeatable but it still doesn't give me any verification that item A will happen 80% of the time (for example).

So what kind of approach can I use to write test coverage for random probabilities?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

浅浅淡淡 2024-11-15 07:48:54

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

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

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

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

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

I think you should separate your goals. One is to stub Kernel.rand as you mention. With rspec for example, you can do something like this:

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

Note that this stub won't work unless you call rand with Kernel as the receiver. If you just call "rand" then the current "self" will receive the message, and you'll actually get a random number instead of the test_values.

The second goal is to do something like a field test where you actually generate random numbers. You'd then use some kind of tolerance to ensure you get close to the desired percentage. This is never going to be perfect though, and will probably need a human to evaluate the results. But it still is useful to do because you might realize that another random number generator might be better, like reading from /dev/random. Also, it's good to have this kind of test because let's say you decide to migrate to a new kind of platform whose system libraries aren't as good at generating randomness, or there's some bug in a certain version. The test could be a warning sign.

It really depends on your goals. Do you only want to test your weighting algorithm, or also the randomness?

生生漫 2024-11-15 07:48:54

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

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

It's best to stub Kernel.rand to return fixed values.

Kernel.rand is not your code. You should assume it works, rather than trying to write tests that test it rather than your code. And using a fixed set of values that you've chosen and explicitly coded in is better than adding a dependency on what rand produces for a specific seed.

我恋#小黄人 2024-11-15 07:48:54

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

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

引用文档(添加强调):

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

返回先前的种子值。还
请参阅内核::rand。

If you wanna go down the consistent seed route, look at Kernel#srand:

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

To quote the docs (emphasis added):

Seeds the pseudorandom number
generator to the value of number. If
number is omitted or zero, seeds the
generator using a combination of the
time, the process id, and a sequence
number. (This is also the behavior if
Kernel::rand is called without
previously calling srand, but without
the sequence.) By setting the seed
to a known value, scripts can be made
deterministic during testing.
The
previous seed value is returned. Also
see Kernel::rand.

春花秋月 2024-11-15 07:48:54

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

For testing, stub Kernel.rand with the following simple but perfectly reasonable LCPRNG:

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

You might want to skip the division and use the integer result directly if your code is compatible, as all bits of the result would then be repeatable instead of just "most of them". This isolates your test from "improvements" to Kernel.rand and should allow you to test your distribution curve.

我喜欢麦丽素 2024-11-15 07:48:54

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

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

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

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

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

My suggestion: Combine #2 and #3. Set a random seed, then run your tests a very large number of times.

I do not like #1, because it means your test is super-tightly coupled to your implementation. If you change how you are using the output of rand(), the test will break, even if the result is correct. The point of a unit test is that you can refactor the method and rely on the test to verify that it still works.

Option #3, by itself, has the same problem as #1. If you change how you use rand(), you will get different results.

Option #2 is the only way to have a true black box solution that does not rely on knowing your internals. If you run it a sufficiently high number of times, the chance of random failure is negligible. (You can dig up a stats teacher to help you calculate "sufficiently high," or you can just pick a really big number.)

But if you're hyper-picky and "negligible" isn't good enough, a combination of #2 and #3 will ensure that once the test starts passing, it will keep passing. Even that negligible risk of failure only crops up when you touch the code under test; as long as you leave the code alone, you are guaranteed that the test will always work correctly.

第几種人 2024-11-15 07:48:54

通常,当我需要从随机数派生的结果中获得可预测的结果时,我通常希望控制 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)

Pretty often when I need predictable results from something that is derived from a random number I usually want control of the RNG, which means that the easiest is to make it injectable. Although overriding/stubbing rand can be done, Ruby provides a fine way to pass your code a RNG that is seeded with some value:

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

and then inject a Random object I make on the spot in the test, with a known seed:

rng = Random.new(782199) # Scientific dice roll
compute_random_based_value(your_input, random: rng)
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文