测试这个的最佳方法是什么?

时间:2010-01-17 22:50:23

标签: ruby unit-testing

我正在浏览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.

这显然让我思考:什么是可靠地测试随机事物的最佳方法(特别是,通常)?

14 个答案:

答案 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)

我认为测试任何涉及随机性的方法的最佳方法是统计学上的。循环运行你的骰子功能一百万次,将结果制成表格,然后对结果进行一些假设检验。一百万个样本应该给你足够的统计功效,几乎所有与正确代码的偏差都会被注意到。您希望演示两个统计属性:

  1. 每个值的概率都是您的意图。
  2. 所有名单都是相互独立的事件。
  3. 您可以使用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)

对我来说,这似乎有点傻。您是否应该测试(伪)随机数生成器是否生成随机数?那是徒劳无益的。如果有的话,你可以测试那个dice.roll给你的PRNG打电话。

答案 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,并在第二次滚动之前使用不同的数字进行srand。这样可以防止重复这个系列。

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