Каковы хорошие модульные тесты, чтобы покрыть случай использования броска матрицы?

18

Я пытаюсь справиться с модульным тестированием.

Скажем, у нас есть кубик, у которого по умолчанию число сторон равно 6 (но может быть 4, 5 сторон и т. Д.):

import random
class Die():
    def __init__(self, sides=6):
        self._sides = sides

    def roll(self):
        return random.randint(1, self._sides)

Будут ли следующие действительные / полезные юнит-тесты?

  • проверить бросок в диапазоне 1-6 для 6-ти стороннего штампа
  • проверить бросок 0 для 6-ти стороннего штампа
  • проверить бросок 7 для 6-ти стороннего штампа
  • проверить бросок в диапазоне 1-3 для 3-х стороннего штампа
  • проверить бросок 0 для 3-х стороннего штампа
  • проверить бросок 4 для 3-х стороннего штампа

Я просто думаю, что это пустая трата времени, так как случайный модуль существует достаточно долго, но потом я думаю, что если случайный модуль будет обновлен (скажем, я обновляю свою версию Python), то, по крайней мере, меня охватят.

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

Cybran
источник
1
А как насчет кубика с минус 5-мя сторонами или кубика с нулевой стороной?
JensG

Ответы:

22

Вы правы, ваши тесты не должны проверять, что randomмодуль выполняет свою работу; unittest должен только проверять сам класс, а не то, как он взаимодействует с другим кодом (который должен быть проверен отдельно).

Конечно, вполне возможно, что ваш код использует random.randint()неправильно; или вы random.randrange(1, self._sides)вместо этого звоните, и ваш кубик никогда не выбрасывает наивысшее значение, но это будет другой тип ошибки, а не тот, который вы могли бы обнаружить с помощью юнит-теста. В этом случае ваше die устройство работает так, как задумано, но сама конструкция была ошибочной.

В этом случае, я хотел бы использовать насмешливый , чтобы заменить на randint()функцию, и только проверить , что это было называется правильно. Python 3.3 и выше поставляется с unittest.mockмодулем для выполнения этого типа тестирования, но вы можете установить внешний mockпакет на более старые версии, чтобы получить точно такую ​​же функциональность

import unittest
try:
    from unittest.mock import patch
except ImportError:
    # < python 3.3
    from mock import patch


@patch('random.randint', return_value=3)
class TestDice(unittest.TestCase):
    def _make_one(self, *args, **kw):
        from die import Die
        return Die(*args, **kw)

    def test_standard_size(self, mocked_randint):
        die = self._make_one()
        result = die.roll()

        mocked_randint.assert_called_with(1, 6)
        self.assertEqual(result, 3)

    def test_custom_size(self, mocked_randint):
        die = self._make_one(sides=42)
        result = die.roll()

        mocked_randint.assert_called_with(1, 42)
        self.assertEqual(result, 3)


if __name__ == '__main__':
    unittest.main()

С насмешками ваш тест теперь очень прост; на самом деле есть только 2 случая. Случай по умолчанию для 6-стороннего кристалла и пользовательский случай.

Существуют и другие способы временно заменить randint()функцию в глобальном пространстве имен Die, но mockмодуль делает это проще всего. @mock.patchДекоратор здесь применяется ко всем методам испытаний в тесте; каждому тестовому методу передается дополнительный аргумент, random.randint()проверяемая функция, поэтому мы можем проверить макет, чтобы убедиться, что он действительно был вызван правильно. В return_valueаргумент указывает , что возвращаемые из издеваться , когда его называют, так что мы можем проверить , что die.roll()метод действительно вернул «случайный» результат для нас.

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

Таким образом, если вы допустили ошибку в самом коде модуля, тесты все равно будут запущены; они просто потерпят неудачу, сообщая вам об ошибке в вашем коде.

Чтобы было ясно, вышеприведенные тесты чрезвычайно упрощены. random.randint()Например, цель не в том, чтобы проверить, что было вызвано с правильными аргументами. Вместо этого цель состоит в том, чтобы проверить, что устройство дает правильные результаты при определенных входных данных, где эти входные данные включают результаты других не тестируемых устройств. Посмеиваясь над этим random.randint()методом, вы получаете контроль над еще одним вводом кода.

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

Например, в коде, который проверяет подлинность пользователей на основе сторонней службы OAuth2 (многоэтапное взаимодействие), вы хотите проверить, передает ли ваш код нужные данные этой сторонней службе и позволяет ли вы выводить различные сообщения об ошибках, которые Сторонний сервис вернется, что позволит вам моделировать различные сценарии, не создавая полноценный сервер OAuth2 самостоятельно. Здесь важно проверить, что информация из первого ответа была правильно обработана и передана вызову второго этапа, поэтому вы хотите убедиться, что проверяемая служба вызывается правильно.

Мартейн Питерс
источник
1
У вас есть несколько более чем 2 тестовых случая ... результаты проверки для значения по умолчанию: нижний (1), верхний (6), нижний нижний (0), за верхний (7) и результаты для указанных пользователем чисел, таких как max_int и т. д. вход также не проверен, что может потребовать проверки в какой-то момент ...
Джеймс Снелл
2
Нет, это тесты randint(), а не код в Die.roll().
Мартин Питерс
На самом деле есть способ убедиться, что не просто randint вызывается правильно, но и что его результат также используется правильно: смоделируйте его, чтобы вернуть, sentinel.dieнапример (объект sentinel unittest.mockтоже), а затем убедитесь, что это то, что было возвращено из вашего метода roll. Это фактически позволяет только один способ реализации проверенного метода.
aragaer
@aragaer: конечно, если вы хотите убедиться, что значение возвращается без изменений, sentinel.dieэто был бы отличный способ убедиться в этом.
Мартейн Питерс
Я не понимаю, почему вы хотите убедиться, что mocked_randint вызывается с определенными значениями. Я понимаю, что хочу, чтобы mond randint возвращал предсказуемые значения, но разве проблема не в том, что он возвращает предсказуемые значения, а не в каких значениях он вызывается? Мне кажется, что проверка вызываемых значений излишне привязывает тест к мелким деталям реализации. Кроме того, почему мы заботимся о том, чтобы матрица возвращала точное значение randint? Разве нас не волнует, что оно возвращает значение> 1 и меньше, чем равно макс?
bdrx
16

Мартейн ответит , как бы вы это сделали, если бы вы действительно хотели запустить тест, который показывает, что вы звоните random.randint. Однако, рискуя получить ответ «это не отвечает на вопрос», я чувствую, что это вообще не должно быть модульным тестированием. Дразнящий randint больше не черный ящик тестирования - вы определенно показывает , что некоторые вещи происходят в реализации . Тестирование черного ящика - это даже не вариант - нет теста, который вы можете выполнить, который докажет, что результат никогда не будет меньше 1 или больше 6.

Вы можете издеваться randint? Да, ты можешь. Но что ты доказываешь? Это вы назвали это аргументами 1 и сторонами. Что , что среднее? Вы вернулись на круги своя - в конце дня вам придется доказать - формально или неформально - что вызов random.randint(1, sides)правильно реализует бросок костей.

Я все для модульного тестирования. Они являются фантастическими проверками работоспособности и выявляют наличие ошибок. Тем не менее, они никогда не смогут доказать свое отсутствие, и есть вещи, которые вообще не могут быть подтверждены тестированием (например, что конкретная функция никогда не вызывает исключение или всегда завершается.) В этом конкретном случае я чувствую, что вы очень мало чего стоите усиление. Для поведения, которое является детерминированным, модульные тесты имеют смысл, потому что вы действительно знаете, какой ответ вы ожидаете получить.

Doval
источник
Юнит-тесты - это не тесты черного ящика. Вот для чего нужны интеграционные тесты, чтобы убедиться, что различные части взаимодействуют так, как задумано. Конечно, это вопрос мнения (большинство философии тестирования), см. Подпадает ли «модульное тестирование» под белый или черный ящик? и модульное тестирование черного ящика для некоторых перспектив (переполнение стека).
Мартейн Питерс
@MartijnPieters Я не согласен с тем, что «для этого нужны интеграционные тесты». Интеграционные тесты предназначены для проверки правильности взаимодействия всех компонентов системы. Они не место для проверки того, что данный компонент дает правильный вывод для данного ввода. Что касается «черного ящика» по сравнению с модульным тестированием «белого ящика», то модульные тесты «белого ящика» со временем будут ломаться с изменениями реализации, и любые предположения, сделанные вами в реализации, вероятно, будут перенесены в тест. Проверка того, что random.randintвызывается с помощью 1, sides, бесполезна, если это неправильно.
Довал
Да, это ограничение модульного теста белого ящика. Тем не менее, нет смысла в тестировании, которое random.randint()будет правильно возвращать значения в диапазоне [1, стороны] (включительно), это зависит от разработчиков Python, чтобы убедиться, что randomмодуль работает правильно.
Мартейн Питерс
И, как вы говорите сами, модульное тестирование не может гарантировать, что ваш код не содержит ошибок; если ваш код использует другие модули неправильно (скажем, вы ожидали, что будете random.randint()вести себя как random.randrange()и, следовательно, вызывать его с помощью random.randint(1, sides + 1), то вы все равно утонете.
Мартин Питерс
2
@MartijnPieters Я согласен с вами там, но я не против этого. Я возражаю против проверки того, что random.randint вызывается с аргументами (1, стороны) . В реализации вы предположили, что это правильно, и теперь вы повторяете это предположение в тесте. Если это предположение будет неверным, тест пройден, но ваша реализация по-прежнему неверна. Это недоделанное доказательство, это полная боль в заднице, чтобы писать и поддерживать.
Довал
6

Исправить случайное семя. Для 1, 2, 5 и 12-гранных костей подтвердите, что несколько тысяч бросков дают результаты, включая 1 и N, и не включая 0 или N + 1. Если, по всей вероятности, вы получите набор случайных результатов, которые не дают охватить ожидаемый диапазон, переключиться на другое семя.

Инструменты издевательства - это круто, но если они позволяют вам что-то делать, это еще не значит, что это нужно делать. YAGNI относится как к тестовым приборам, так и к функциям.

Если вы можете легко тестировать с немодулированными зависимостями, вы всегда должны это делать; Таким образом, ваши тесты будут направлены на уменьшение количества дефектов, а не только на увеличение количества тестов. Чрезмерные насмешливые риски приводят к вводящим в заблуждение цифрам покрытия, что, в свою очередь, может привести к переносу фактического тестирования на более поздний этап, к которому у вас, возможно, никогда не будет времени вернуться

Сору
источник
3

Что делать, Dieесли вы думаете об этом? - не более, чем обертка вокруг random. Он инкапсулирует random.randintи relabels его с точки зрения собственного словарного запаса вашего приложения: Die.Roll.

Я не считаю уместным вставлять еще один уровень абстракции между собой, Dieи randomпотому что Dieсам по себе уже является этим уровнем перенаправления между вашим приложением и платформой.

Если ты хочешь получить результаты игры в кости, просто издевайся Die, не издевайсяrandom .

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

=> Учитывая, что Dieэто всего лишь несколько тривиальных строк кода и мало что добавляет к логике по сравнению с randomсамим собой, я бы пропустил тестирование в этом конкретном примере.

guillaume31
источник
2

Насколько я вижу, заполнение генератора случайных чисел и проверка ожидаемых результатов НЕ является действительным тестом. Это делает предположения относительно того, КАК ваша игра в кости внутренне, которая является непослушной. Разработчики python могут изменить генератор случайных чисел или кубик (ПРИМЕЧАНИЕ: «кубик» - это множественное число, «кубик» - единственное число. Если ваш класс не реализует несколько бросков кубика за один вызов, его, вероятно, следует назвать «кубик») использовать другой генератор случайных чисел.

Точно так же насмешка над случайной функцией предполагает, что реализация класса работает точно так, как ожидалось. Почему это не так? Кто-то может взять под контроль генератор случайных чисел по умолчанию на python, и во избежание этого будущая версия вашего кубика может выбрать несколько случайных чисел или большее число случайных чисел, чтобы смешать больше случайных данных. Аналогичная схема использовалась создателями операционной системы FreeBSD, когда они подозревали, что АНБ вмешивается в аппаратные генераторы случайных чисел, встроенные в ЦП.

Если бы это был я, я бы выполнил, скажем, 6000 бросков, подсчитал их и убедился, что каждое число от 1 до 6 прокручивается от 500 до 1500 раз. Я также проверил бы, что никакие числа вне того диапазона не возвращены. Я мог бы также проверить, что для второго набора из 6000 бросков при упорядочении [1..6] по порядку частоты результат будет другим (это не удастся выполнить один раз из 720 запусков, если числа случайны!). Если вы хотите быть точным, вы можете найти частоту чисел, следующих за 1, после 2 и т. Д .; но убедитесь, что ваш размер выборки достаточно большой, и у вас достаточно дисперсии. Люди ожидают, что случайные числа будут иметь меньше шаблонов, чем на самом деле.

Повторите эти действия для 12-стороннего и 2-стороннего кристалла (наиболее часто используется 6, поэтому наиболее ожидаемо для любого, кто пишет этот код).

Наконец, я хотел бы проверить, что происходит с 1-сторонним штампом, 0-сторонним штампом, -1-сторонним штампом, 2,3-сторонним штампом, [1,2,3,4,5,6] двусторонним штампом и умирающий "бла" Конечно, все они должны потерпеть неудачу; они терпят неудачу полезным способом? Вероятно, они должны потерпеть неудачу при создании, а не при переходе

Или, может быть, вы хотите обрабатывать их по-другому - возможно, создание кубика с [1,2,3,4,5,6] должно быть приемлемым - и, возможно, также "бла"; это может быть кубик с четырьмя лицами, на каждом из которых есть буква. Игра «Boggle» приходит на ум, как и волшебный шар из восьми.

И наконец, вы можете рассмотреть это: http://lh6.ggpht.com/-fAGXwbJbYRM/UJA_31ACOLI/AAAAAAAAAPg/2FxOWzo96KE/s1600-h/random%25255B3%25255D.jpg

AMADANON Inc.
источник
2

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

Моя стратегия заключалась в том, чтобы просто издеваться над ГСЧ, который создает предсказуемый поток значений, охватывающий все пространство. Если (скажем) сторона = 6, и ГСЧ выдает значения от 0 до 5 в последовательности, я могу предсказать, как должен вести себя мой класс, и провести модульное тестирование соответственно.

Обоснование состоит в том, что это проверяет логику только в этом классе, исходя из предположения, что ГСЧ в конечном итоге будет производить каждое из этих значений и без проверки самого ГСЧ.

Это просто, детерминистично, воспроизводимо и ловит ошибки. Я бы снова использовал ту же стратегию.


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

david.pfx
источник
Скажем, вы издеваетесь над ГСЧ, чтобы быть предсказуемым. Ну что ты тогда тестируешь? Вопрос задает вопрос: «Будут ли следующие тесты действительными / полезными?» Насмешка, чтобы вернуть 0-5, это не тест, а скорее тестовая настройка. Как бы вы "модульный тест соответственно"? Я не понимаю, как он "ловит ошибки". Мне трудно понять, что мне нужно для «модульного» тестирования.
bdrx
@bdrx: Это было некоторое время назад: сейчас я бы ответил по-другому. Но смотрите редактировать.
david.pfx
1

Тесты, которые вы предлагаете в своем вопросе, не обнаруживают модульный арифметический счетчик как реализацию. И они не обнаруживают типичные ошибки реализации в коде, связанном с распределением вероятностей return 1 + (random.randint(1,maxint) % sides). Или изменение в генераторе, которое приводит к 2-мерным образцам.

Если вы действительно хотите убедиться, что вы генерируете равномерно распределенные случайные числа, вам нужно проверить очень широкий спектр свойств. Чтобы сделать достаточно хорошую работу, вы можете запустить http://www.phy.duke.edu/~rgb/General/dieharder.php на свои сгенерированные номера. Или напишите такой же сложный набор юнит-тестов.

Это не вина модульного тестирования или TDD, случайность просто очень трудно проверить. И популярная тема для примеров.

Патрик
источник
-1

Самый простой тест броска кубика - просто повторить его несколько сотен тысяч раз и проверить, что каждый возможный результат был получен примерно (1 / количество сторон) раз. В случае 6-стороннего кубика вы должны увидеть, как каждое возможное значение поражает примерно 16,6% времени. Если какие-либо из них более чем на процент, то у вас есть проблемы.

Это позволяет избежать рефакторинга базовой механики генерации случайного числа легко и, что наиболее важно, без изменения теста.

ChristopherBrown
источник
1
этот тест прошел бы для совершенно неслучайной реализации, которая просто перебирает стороны одну за другой в предопределенном порядке
gnat
1
Если кодер намеревается внедрить что-то недобросовестно (не используя рандомизирующий агент на кристалле) и просто попытаться найти что-то, чтобы «заставить красные огни загореться», у вас больше проблем, чем может реально решить юнит-тестирование.
КристоферБрау