Які хороші одиничні тести для висвітлення випадку використання кочення штампів?


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 у цьому випадку, чи добре покрити інший ініціалізований стан штампу?


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().
Martijn Pieters

Насправді існує спосіб переконатись у тому, що не просто виклик randint називається правильно, але і його результат використовується правильно: знущайтеся над ним, щоб повернути sentinel.dieнаприклад (дозорний об’єкт unittest.mockтеж), а потім перевірте, що це те, що було повернуто з вашого методу roll. Це фактично дозволяє лише один спосіб реалізації випробуваного методу.
араґер

@aragaer: впевнений, що якщо ви хочете переконатися, що значення повертається незмінним, sentinel.dieце був би чудовий спосіб забезпечити це.
Martijn Pieters

Я не розумію, чому ви хочете переконатися, що mocked_randint називається_з певними значеннями. Я розумію, що хочу знущатися над randint, щоб повернути передбачувані значення, але хіба це питання не лише в тому, що він повертає передбачувані значення, а не те, з якими значеннями він викликається? Мені здається, що перевірка викликаних значень зайво прив'язує тест до тонких деталей реалізації. Також чому ми дбаємо про те, щоб матриця повертала точне значення randint? Хіба нас насправді не хвилює, що вона повертає значення> 1 і менше, ніж дорівнює максимуму?
bdrx

16

Відповідь Мартійна полягає в тому, як би ви це зробили, якби дійсно хотіли провести тест, який демонструє, що ви викликаєте random.randint. Однак, ризикуючи сказати, що "не відповідає на питання", я вважаю, що це взагалі не повинно бути перевіреним. Знущання над randint - це вже не тестування чорної скриньки - ви конкретно показуєте, що певні речі тривають у впровадженні . Тестування чорної скриньки це навіть не варіант - не існує тесту, який можна виконати, який підтвердить, що результат ніколи не буде меншим за 1 або більше 6.

Ви можете знущатися randint? Так, ти можеш. Але що ти доводиш? Щоб ви назвали це аргументами 1 і сторонами. Що , що середнє? Ви знову в квадратному - наприкінці дня вам доведеться довести - формально чи неофіційно - що виклик random.randint(1, sides)правильно реалізує рулон з кістки.

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


Тести одиниці - це не чорні тести. Ось для чого призначені тести на інтеграцію, щоб переконатися, що різні частини взаємодіють як задумано. Це питання думки, звичайно (більшість філософії тестування), див. Чи підпадає під "тестування блоку" під тестування білого або чорного поля? і тестування блоку чорної скриньки на деякі перспективи (переповнення стека).
Martijn Pieters

@MartijnPieters Я не погоджуюсь, що "для цього потрібні тести на інтеграцію". Інтеграційні тести призначені для перевірки правильності взаємодії всіх компонентів системи. Їм не місце перевірити, чи даний компонент дає правильний вихід для даного входу. Що стосується тестування блоку «чорна скринька» та «білого поля», то тести блоку білого поля в кінцевому рахунку будуть порушені із змінами впровадження, і будь-які припущення, які ви зробили в процесі впровадження, швидше за все перенесуть у тест. Перевірка того, що random.randintвикликається 1, sides, не має сенсу, якщо це неправильно робити.
Доваль

Так, це обмеження тесту одиниць білого поля. Однак немає сенсу тестувати, що random.randint()буде правильно повертати значення в діапазоні [1, сторони] (включно), це залежить від розробників Python, щоб переконатися, що randomпристрій працює правильно.
Martijn Pieters

І як ви самі кажете, тестування приладів не може гарантувати, що ваш код не містить помилок; якщо ваш код неправильно використовує інші підрозділи (скажімо, ви розраховували random.randint()на те, що ти поводитимешся так, random.randrange()і таким чином викликаєш його random.randint(1, sides + 1), то все одно затонув.
Martijn Pieters

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самим собою, я пропустив би тестування цього конкретного прикладу.


2

Насіння генератора випадкових чисел та перевірка очікуваних результатів НЕ, наскільки я бачу, є достовірним тестом. Це робить припущення щодо того, як ваші кістки працюють внутрішньо, що неслухняно-неслухняно. Розробники python можуть змінити генератор випадкових чисел або die (ПРИМІТКА: "кістки" множини, "die" є єдиним числом. Якщо ваш клас не реалізує декілька роликів в одному дзвінку, він, ймовірно, може називатися "die") використовувати інший генератор випадкових чисел

Аналогічно, глузування з випадкової функції передбачає, що реалізація класу працює точно так, як очікувалося. Чому це може бути не так? Хтось може взяти під контроль генератор випадкових чисел пітонів за замовчуванням, і щоб уникнути цього, майбутня версія вашої матриці може отримати кілька випадкових чисел або більших випадкових чисел, щоб змішати більше випадкових даних. Аналогічну схему використовували виробники операційної системи FreeBSD, коли вони підозрювали, що NSA підлаштовує апаратні генератори випадкових чисел, вбудовані в процесори.

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

Повторіть для 12-сторонніх та двосторонніх штампів (6 - це найчастіше використовуваний, тому найбільше очікують, хто пише цей код).

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

Або, можливо, ви хочете занадто поводитись із цим по-різному - можливо, створення штампу [1,2,3,4,5,6] має бути прийнятним - і, можливо, також "благ"; це може бути плашка з чотирма гранями, і кожне обличчя має літеру. Гра "Бряглиця" припадає на розум, як і чарівний вісім м'яч.

І нарешті, ви можете задуматися над цим: http://lh6.ggpht.com/-fAGXwbJbYRM/UJA_31ACOLI/AAAAAAAAAPg/2FxOWzo96KE/s1600-h/random%25255B3%25255D.jpg


2

Загрожуючи плаванням проти припливу, я вирішив цю точну проблему кілька років тому, використовуючи метод, який не так часто згадувався.

Моя стратегія полягала в тому, щоб просто знущатися над RNG з таким, який створює передбачуваний потік значень, що охоплює весь простір. Якщо (скажімо) сторона = 6 і RNG створює значення від 0 до 5 послідовно, я можу передбачити, як повинен вести себе мій клас, і одиничне тестування відповідно.

Обґрунтування полягає в тому, що це випробовує логіку лише для цього класу, припускаючи, що RNG врешті-решт виробить кожне з цих значень і без тестування самої RNG.

Він простий, детермінований, відтворюваний, і він вловлює помилок. Я б знову застосував ту саму стратегію.


Питання не уточнює, якими повинні бути тести, а які дані можуть бути використані для тестування, враховуючи наявність RNG. Моя пропозиція - це просто вичерпно перевірити, глузуючи з RNG. Питання про те, що варто перевірити, залежить від інформації, не наданої у запитанні.


Скажіть, ви знущаєтесь над RNG передбачувано. Ну що ви тоді тестуєте? Питання задає питання: "Чи були б наступні правильні / корисні одиничні тести?" Знущання з нього, щоб повернути 0-5, не є тестом, а скоріше тестовою установкою. Як би ви "відповідно провели тест"? Я не розумію, як це "вловлює помилок". Мені важко зрозуміти, що мені потрібно для "одиничного" тесту.
bdrx

@bdrx: Це було певний час тому: я зараз би відповів на це по-іншому. Але дивіться редагувати.
david.pfx

1

Тести, які ви пропонуєте у своєму запитанні, не виявляють модульний арифметичний лічильник як реалізацію. І вони не виявляють поширених помилок впровадження в подібному коді, пов'язаному з розподілом ймовірностей return 1 + (random.randint(1,maxint) % sides). Або зміна генератора, що призводить до двовимірних візерунків.

Якщо ви насправді хочете перевірити, що ви генеруєте рівномірно розподілені випадкові числа, вам потрібно перевірити дуже широкий спектр властивостей. Щоб зробити досить хорошу роботу в цьому, ви можете запустити http://www.phy.duke.edu/~rgb/General/dieharder.php на створених номерах. Або написати аналогічно складний набір одиничних тестів.

Це не вина тестування одиниць або TDD, випадковість просто здається дуже важкою властивістю перевірити. І популярна тема для прикладів.


-1

Найпростіший тест діл-рулону - це просто повторити його кілька сотень тисяч разів і перевірити, що кожен можливий результат був вражений приблизно (1 / кількість сторін) разів. У випадку 6-стороннього штампу ви повинні бачити, що кожне можливе значення потрапляє приблизно в 16,6% часу. Якщо будь-які вимкнені більш ніж на відсоток, то у вас є проблеми.

Це дозволяє уникнути можливого відновлення основної механіки генерації випадкового числа легко, а головне, не змінюючи тест.


1
цей тест пройшов би для абсолютно невипадкової реалізації, яка просто переводиться через сторони одна за одною у заздалегідь визначеному порядку
gnat

1
Якщо кодер має намір реалізувати щось недобросовісно (не використовуючи рандомізуючий агент на штампі) та просто намагаючись знайти щось для того, щоб «червоні вогні стали зеленими», у вас є більше проблем, ніж тестування одиниць реально може вирішити.
КрістоферБроун
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.