代码之家  ›  专栏  ›  技术社区  ›  Matthew Groves

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

  •  15
  • Matthew Groves  · 技术社区  · 15 年前

    我要穿过Edgecase Ruby Koans。在 about_dice_project.rb 有一个测试叫做“测试骰子的值应该在两个骰子之间改变”,这很简单:

      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.
    

    哪一个(显然)让我想到:什么是可靠地测试这种随机事件的最好方法(具体地说,一般地)?

    13 回复  |  直到 6 年前
        1
  •  18
  •   dsimcha    15 年前

    我想说,测试任何涉及随机性的东西的最好方法是统计。在循环中运行骰子函数一百万次,将结果制成表格,然后对结果进行一些假设测试。一百万个样本会给你足够的统计能力,几乎所有偏离正确代码的地方都会被注意到。您希望演示两个统计特性:

    1. 每一个值的概率都是你想要的。
    2. 所有滚动都是相互独立的事件。

    您可以使用 Pearson's Chi-square test. 如果你使用的是一个好的随机纳伯发生器,比如 Mersenne Twister (这是大多数现代语言的标准LIB中的默认值,虽然不是C和C++),而且除了Melsern捻线生成器本身之外,您没有使用任何保存的状态,那么您的卷对于所有实用目的是相互独立的。

    作为随机函数统计测试的另一个例子,当我 ported the NumPy random number generators to the D programming language ,我测试端口是否正确的方法是使用 Kolmogorov-Smirnov test 看看生成的数字是否与它们应该匹配的概率分布相匹配。

        2
  •  21
  •   Bart Tomer W    13 年前

    imho到目前为止,除了@super_dummy之外,大多数答案都没有抓住koan问题的要点。让我详细阐述一下我的想法…

    说这个,而不是掷骰子,我们是在掷硬币。再加上在我们的集合中只使用一个硬币的另一个约束,我们有一个最小的非平凡集合,可以生成“随机”结果。

    如果我们想检查每次翻动“硬币组”(在本例中是一枚硬币)会产生不同的结果,我们希望 价值观 在统计的基础上,每一个单独的结果在50%的时间内是相同的。运行 那个 单元测试通过 n 一些大型的迭代 n 只需练习prng。它没有实质性地告诉你两个结果之间的实际相等或不同。

    换一种说法,在这个游戏中,我们实际上并不关心每一卷骰子的价值。我们更关心的是,返回的卷实际上是不同卷的表示。检查返回值是否不同只是一阶检查。

    大部分时间都是足够的——但偶尔,随机性会导致单元测试失败。这不是好事

    如果,在两个连续滚动返回相同结果的情况下,我们应该检查两个结果实际上由不同的对象表示。这将允许我们在将来重构代码(如果需要的话),同时确信测试仍然可以 总是 捕获任何行为不正确的代码。

    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
    
        3
  •  10
  •   tfwright    15 年前

    没有办法为随机性编写基于状态的测试。它们是矛盾的,因为基于状态的测试通过提供已知的输入和检查输出来进行。如果您的输入(随机种子)未知,则无法进行测试。

    幸运的是,您并不真正想测试Rand for Ruby的实现,因此您可以使用 mocha .

    def test_roll
      Kernel.expects(:rand).with(5).returns(1)
      Diceset.new.roll(5)
    end
    
        4
  •  7
  •   Ken    15 年前

    这里好像有两个独立的单元。首先,一个随机数生成器。第二,使用(P)RNG的“骰子”抽象。

    如果您想对骰子抽象进行单元测试,那么模拟出prng调用,并确保它调用它们,并为您提供的输入返回适当的值,等等。

    prng可能是您的库/框架/OS的一部分,所以我不想费心测试它。也许你会想要一个 整合 测试它是否返回合理的值,但这不是一个完整的问题。

        5
  •  6
  •   axel22    13 年前

    不是比较值,而是比较 object_id :

        assert_not_equal first_time.object_id, second_time.object_id
    

    这假设其他测试将检查整数数组。

        6
  •  3
  •   Jarrett Meyer    14 年前

    我的解决方案是允许一个块传递给roll函数。

    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的调用。它不会改变标准功能。

        7
  •  2
  •   YankovskyAndrey    12 年前

    每次调用Roll方法时只需创建新数组。这样你就可以使用

    assert_not_same first_time, second_time,
    "Two rolls should not be equal"
    

    测试对象的一致性。 是的,这个测试依赖于实现,但是没有方法测试随机性。 另一种方法是使用弗洛伊德建议的模拟。

        8
  •  1
  •   Andy_Vulhop    14 年前

    我觉得这有点傻。您是否应该测试(psuedo)随机数生成器是否正在生成随机数?那是徒劳无益的。如果有的话,你可以测试对你的prng的dice.roll调用。

        9
  •  1
  •   MurifoX    13 年前

    我用递归解决了这个问题:

    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 方法传递给测试变量,因此它不会将引用传递给我的实例变量 @价值观 .

    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
    
        10
  •  1
  •   gusa    12 年前

    兰德是决定性的,它依赖于它的种子。在第一卷前使用给定编号的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"
    
        11
  •  1
  •   Dolev    6 年前

    伊姆霍, 随机性 应该用 依赖注入 .

    斯基特 回答了如何测试随机性的一般答案 here

    我建议您将随机性的来源(随机数生成器或其他)视为依赖项。然后,您可以通过提供一个伪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)
    
        12
  •  0
  •   Aditya    12 年前

    我刚创建了一个新实例

    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
    
        13
  •  -1
  •   YankovskyAndrey    12 年前

    我通过在每次调用“roll”方法时为每个骰子创建一组新的值来解决这个问题:

    def roll(n)     
        @numbers = []
        n.times do
          @numbers << rand(6)+1
        end
    end