Какой лучший способ проверить это? - PullRequest
15 голосов
/ 18 января 2010

Я прохожу через 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.

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

Ответы [ 13 ]

21 голосов
/ 16 декабря 2011

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

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

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

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

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

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

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
18 голосов
/ 18 января 2010

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

  1. Вероятность каждого значения соответствует ожидаемой.
  2. Все броски являются взаимно независимыми событиями.

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

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

10 голосов
/ 18 января 2010

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

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

def test_roll
  Kernel.expects(:rand).with(5).returns(1)
  Diceset.new.roll(5)
end
7 голосов
/ 18 января 2010

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

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

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

6 голосов
/ 29 августа 2011

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

    assert_not_equal first_time.object_id, second_time.object_id

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

3 голосов
/ 28 сентября 2010

Мое решение состояло в том, чтобы разрешить передачу блока функции крена.

class DiceSet
  def roll(n)
    @values = (1..n).map { block_given? ? yield : rand(6) + 1 }
  end
end

Затем я могу передать свой собственный ГСЧ в тесты, подобные этому.не знаю, действительно ли это лучше, но это абстрагирует вызовы к ГСЧ.И это не меняет стандартную функциональность.

2 голосов
/ 25 августа 2012

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

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

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

1 голос
/ 12 марта 2018

ИМХО, случайность должна быть проверена с инъекцией зависимости .

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

Я предлагаю вам рассматривать ваш источник случайности (генератор случайных чисел или что-то еще) как зависимость. Затем вы можете проверить его с известными входными данными, предоставив поддельный ГСЧ или один с известным затравочным материалом. Это удаляет случайность из теста, сохраняя его в реальном коде.

Пример кода в нашем случае может выглядеть примерно так:

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)
1 голос
/ 04 января 2013

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

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

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
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...