我正在浏览EdgeCase Ruby Koans。在about_dice_project.rb中,有一个名为“test_dice_values_should_change_between_rolls”的测试,这很简单:
def test_dice_values_should_change_between_rolls
dice = DiceSet.new
dice.roll(5)
first_time = dice.values
dice.roll(5)
second_time = dice.values
assert_not_equal first_time, second_time,
"Two rolls should not be equal"
end
除了出现在那里的评论:
# THINK ABOUT IT:
#
# If the rolls are random, then it is possible (although not
# likely) that two consecutive rolls are equal. What would be a
# better way to test this.
这显然让我思考:什么是可靠地测试随机事物的最佳方法(特别是,通常)?
答案 0 :(得分:21)
恕我直言,到目前为止,大多数答案都错过了Koan问题,但@Super_Dummy除外。让我详细说明我的想法......
我们正在翻转硬币,而不是骰子。在我们的集合中添加仅使用一个硬币的另一个约束,并且我们有一个可以生成“随机”结果的最小非平凡集。
如果我们想检查翻转“硬币套装”[在这种情况下是单个硬币]每次产生不同的结果,我们希望每个单独结果的值是相同的50%的时间,在统计基础上。对于某些大型 n ,通过 n 迭代运行 单元测试将简单地运行PRNG。它没有告诉你关于两个结果之间的实际平等或差异的实质内容。
换句话说,在这个Koan中,我们实际上并不关心每个掷骰子的价值。我们真的更担心返回的卷实际上是不同卷的表示。检查返回的值是否不同只是一阶检查。
大部分时间都足够 - 但偶尔也会随机性导致您的单元测试失败。这不是一件好事。
如果在两个连续滚动返回相同结果的情况下,我们应该检查两个结果是否实际上由不同的对象表示。这将允许我们在将来重构代码[如果需要],同时确信测试仍然总是捕获任何行为不正确的代码。
TL; DR?
def test_dice_values_should_change_between_rolls
dice = DiceSet.new
dice.roll(5)
first_time = dice.values
dice.roll(5)
second_time = dice.values
assert_not_equal [first_time, first_time.object_id],
[second_time, second_time.object_id], "Two rolls should not be equal"
# THINK ABOUT IT:
#
# If the rolls are random, then it is possible (although not
# likely) that two consecutive rolls are equal. What would be a
# better way to test this.
end
答案 1 :(得分:18)
我认为测试任何涉及随机性的方法的最佳方法是统计学上的。循环运行你的骰子功能一百万次,将结果制成表格,然后对结果进行一些假设检验。一百万个样本应该给你足够的统计功效,几乎所有与正确代码的偏差都会被注意到。您希望演示两个统计属性:
您可以使用Pearson's Chi-square test.测试骰子卷的频率是否大致正确如果您正在使用一个好的随机nunber生成器,例如Mersenne Twister(这是标准库中的默认值)对于大多数现代语言,虽然不适用于C和C ++),并且除了Mersenne Twister生成器本身之外,您没有使用之前卷筒中的任何已保存状态,因此您的卷筒可用于所有实用目的,彼此独立。
作为随机函数统计测试的另一个例子,当我ported the NumPy random number generators to the D programming language时,我对端口是否正确的测试是使用Kolmogorov-Smirnov test来查看生成的数字是否与它们的概率分布相匹配应该匹配。
答案 2 :(得分:10)
无法为随机性编写基于状态的测试。它们是矛盾的,因为基于状态的测试通过提供已知输入和检查输出来进行。如果您的输入(随机种子)未知,则无法进行测试。
幸运的是,你真的不想测试rand for Ruby的实现,所以你可以使用mocha来预期它。
def test_roll
Kernel.expects(:rand).with(5).returns(1)
Diceset.new.roll(5)
end
答案 3 :(得分:7)
这里似乎有2个单独的单位。首先,一个随机数发生器。第二,使用(P)RNG的“骰子”抽象。
如果你想对骰子抽象进行单元测试,那么就嘲笑PRNG调用,并确保它调用它们,并为你给出的输入返回一个合适的值,等等。
PRNG可能是您的库/框架/操作系统的一部分,因此我不打扰它进行测试。也许你会想要一个集成测试来看看它是否返回合理的值,但这是一个完整的'其他问题。
答案 4 :(得分:6)
不是比较值,而是比较object_id
:
assert_not_equal first_time.object_id, second_time.object_id
这假设其他测试将检查整数数组。
答案 5 :(得分:3)
我的解决方案是允许将块传递给滚动功能。
class DiceSet
def roll(n)
@values = (1..n).map { block_given? ? yield : rand(6) + 1 }
end
end
然后我可以将自己的RNG传递给这样的测试。
dice = DiceSet.net
dice.roll(5) { 1 }
first_result = dice.values
dice.roll(5) { 2 }
second_result = dice.values
assert_not_equal first_result, second_result
我不知道这是否真的更好,但它确实抽出了对RNG的调用。它并没有改变标准功能。
答案 6 :(得分:2)
每次调用roll方法时都创建新数组。这样你可以使用
assert_not_same first_time, second_time,
"Two rolls should not be equal"
测试object_id相等性。 是的,这个测试取决于实现,但没有办法测试随机性。 其他方法是使用模拟作为floyd建议。
答案 7 :(得分:1)
答案 8 :(得分:1)
我使用递归来解决问题:
def roll times, prev_roll=[]
@values.clear
1.upto times do |n|
@values << rand(6) + 1
end
roll(times, prev_roll) if @values == prev_roll
end
并且必须在测试变量中添加 dup 方法,因此它不会将引用传递给我的实例变量 @values < / EM> 强>
def test_dice_values_should_change_between_rolls
dice = DiceSet.new
dice.roll(5)
first_time = dice.values.dup
dice.roll(5, first_time)
second_time = dice.values
assert_not_equal first_time, second_time,
"Two rolls should not be equal"
end
答案 9 :(得分:1)
srand(1)
dice.roll(5)
first_time = dice.values
srand(2)
dice.roll(5)
second_time = dice.values
assert_not_equal first_time, second_time,
"Two rolls should not be equal"
答案 10 :(得分:1)
恕我直言,随机性应使用依赖注入进行测试。
Jon Skeet 回答了如何测试随机性的一般答案here
我建议您将随机源(随机数生成器或其他)视为依赖项。然后,您可以通过提供假RNG或具有已知种子的RNG来测试已知输入。这样可以消除测试中的随机性,同时将其保留在实际代码中。
我们案例中的示例代码可能如下所示:
class DependentDiceSet
attr_accessor :values, :randomObject
def initialize(randomObject)
@randomObject = randomObject
end
def roll(count)
@values = Array.new(count) { @randomObject.userRand(1...6) }
end
end
class MyRandom
def userRand(values)
return 6
end
end
class RubyRandom
def userRand(values)
rand(values)
end
end
用户可以注入任何随机行为并测试骰子是否被该行为滚动。我实现ruby随机行为和另一个总是返回6的行为。
用法:
randomDice = DependentDiceSet.new(RubyRandom.new)
sixDice = DependentDiceSet.new(MyRandom.new)
答案 11 :(得分:0)
我刚刚创建了一个新实例
def test_dice_values_should_change_between_rolls
dice1 = DiceSet.new
dice2 = DiceSet.new
dice1.roll(5)
first_time = dice1.values.dup
dice2.roll(5, first_time)
second_time = dice2.values
assert_not_equal first_time, second_time,
"Two rolls should not be equal"
end
答案 12 :(得分:0)
务实的方法是简单地测试更多的纸卷。 (假设此测试适用于两个连续的相同编号的纸卷。)
两个5个辊套的可能性相同=> 6 ** 5 => 1在7776中
两个30个轧辊组的可能性相同=> 6 ** 30 => 221073919720733357899776中为1(地狱冻结的可能性)
这将是简单,高效且准确的[足够]。
(我们不能使用object_id比较,因为测试应该与实现无关,并且实现可以通过使用Array#clear使用相同的数组对象,或者object_id可能已经被重用,但是不太可能)
答案 13 :(得分:-1)
我通过在调用'roll'方法的任何时候为每个骰子创建一组新值来解决它:
def roll(n)
@numbers = []
n.times do
@numbers << rand(6)+1
end
end