Какой лучший способ проверить это?

Я иду через 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.

Что (очевидно) заставило меня задуматься: какой лучший способ надежно проверить что-то случайное (в частности, и вообще)?

Ответы

Ответ 1

Я бы сказал, что лучший способ проверить все, что связано с случайностью, статистически. Запустите функцию кубика в цикле миллион раз, скопируйте результаты, а затем запустите некоторые тесты гипотез по результатам. Миллион образцов должны дать вам достаточную статистическую мощность, что будут замечены почти любые отклонения от правильного кода. Вы хотите продемонстрировать два статистических свойства:

  • Вероятность каждого значения - это то, что вы намеревались сделать.
  • Все ролики являются независимыми друг от друга событиями.

Вы можете проверить, являются ли частоты рулонов кости приблизительными, используя тест Пирсона Чи-квадрат. Если вы используете хороший случайный генератор nunber, такой как Mersenne Twister (который по умолчанию используется в стандартном lib для большинства современных языков, хотя и не для C и С++), и вы не используете какое-либо сохраненное состояние из предыдущих рулонов, кроме самого генератора Mersenne Twister, тогда ваши ролики практически не зависят друг от друга.

В качестве еще одного примера статистического тестирования случайных функций, когда я портировал генераторы случайных чисел NumPy на язык программирования D, мой тест на то, порт был правильным, должен был использовать тест Колмогорова-Смирнова, чтобы узнать, соответствуют ли числа, связанные с вероятностными распределениями, которые они должны были соответствовать.

Ответ 2

ИМХО большинство ответов до сих пор пропустили точку вопроса Koan, за исключением @Super_Dummy. Позвольте мне подробно остановиться на моем мышлении...

Скажите, что вместо кубиков мы переворачивали монеты. Добавьте еще одно ограничение только на использование одной монеты в нашем наборе, и у нас есть минимальное нетривиальное множество, которое может генерировать "случайные" результаты.

Если бы мы хотели проверить, что сбрасывание "набора монет" [в данном случае одной монеты] каждый раз порождало другой результат, мы ожидали бы, что значения каждого отдельного результата будут равны 50% времени, на статистическая основа. Выполнение этой unit test через n итераций для некоторого большого n будет просто осуществлять PRNG. Он ничего не говорит о сути о фактическом равенстве или различии между этими двумя результатами.

Другими словами, в этом Коане мы фактически не заботимся о значениях каждого броска кубиков. Мы действительно больше обеспокоены тем, что возвращенные ролики на самом деле представляют собой разные ролики. Проверка того, что возвращаемые значения различны, - это только проверка первого порядка.

В большинстве случаев этого будет достаточно - но очень редко случайность может привести к сбою вашего unit test. Это не хорошая вещь.

Если в случае, когда два последовательных рулона возвращают одинаковые результаты, мы должны проверить, что два результата фактически представлены разными объектами. Это позволило бы нам повторно преобразовать код в будущем [если это было необходимо], будучи уверенным, что тесты все равно всегда поймают любой код, который не ведет себя корректно.

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

Ответ 3

Невозможно написать тест на основе состояния для случайности. Они противоречивы, поскольку государственные тесты проходят путем предоставления известных входов и проверки выходных данных. Если ваш вход (случайное семя) неизвестен, нет возможности проверить.

К счастью, вы действительно не хотите протестировать реализацию rand для Ruby, поэтому вы можете просто отключить его с ожиданием, используя mocha.

def test_roll
  Kernel.expects(:rand).with(5).returns(1)
  Diceset.new.roll(5)
end

Ответ 4

Кажется, здесь есть 2 отдельных подразделения. Во-первых, генератор случайных чисел. Во-вторых, абстракция "кости", в которой используется (P) RNG.

Если вы хотите unit test абстракцию кубиков, затем извините вызовы PRNG и убедитесь, что они называет их, и возвращает соответствующее значение для ввода, который вы даете, и т.д.

PRNG, вероятно, является частью вашей библиотеки/рамки/ОС, поэтому я бы не стал ее тестировать. Возможно, вам понадобится тест интеграции, чтобы узнать, возвращает ли он разумные значения, но это целая проблема.

Ответ 5

Вместо сравнения значений сравните object_id:

    assert_not_equal first_time.object_id, second_time.object_id

Это предполагает, что другие тесты будут проверять массив целых чисел.

Ответ 6

Мне кажется, немного глупо. Вы должны тестировать, что генератор случайных чисел (psuedo) генерирует случайные числа? Это бесполезно и бессмысленно. Во всяком случае, вы можете проверить, что вызовы dice.roll на ваш PRNG.

Ответ 7

Мое решение состояло в том, чтобы разрешить отправку блока в функцию 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. И это не меняет стандартные функции.

Ответ 8

Просто создайте новый массив каждый раз, когда вы вызываете метод roll. Таким образом вы можете использовать

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

для проверки равенства object_id. Да, этот тест зависит от реализации, но нет возможности проверить случайность. Другой подход заключается в том, чтобы использовать mocks как предложено floyd.

Ответ 9

Я решил проблему с помощью рекурсии:

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 ​​.

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

Я только что создал новый экземпляр

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

Ответ 11

Я решил это, просто создав новый набор значений для каждой кости в любое время, когда вызывается метод "roll":

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

Ответ 12

rand детерминирован и зависит от его семени. Используйте 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"