(Не) занурення Ох сьогоднішні діти. Зіпсуті цими швидкими комп'ютерами і модними "динамічними" мовами. Спочатку напиши, потім публікуй, і вже (якщо дійдуть руки) на третьому місці - зневадження. В наші часи в нас була дисципліна. Дисципліна, Я сказав! Ми мусили писати програми від руки, на папері, і згодовувати їх комп'ютеру на пефокартах. І нам це подобалось!
У цьому розділі, ми збираємось написати та зневадити набір допоміжних функцій для перетворення чисел в римську систему та навпаки. Ви бачили механізми створення та перевірки чисел на приналежність до римських в попередньому прикладі. Тепер давайте відійдемо трохи назад і подумаємо над тим що потрібно зробити щоб перетворити це на двосторонній інструмент.
Правила для римських чисел приводять до багатьох цікавих спостережень.
Існує тільки один коректний спосіб представити конкретне число як римське. Аналогічно і навпаки: якщо рядок символів є правильним римським числом, він представляє собою лише одне число (може бути прочитане однозначно). Існує обмежений діапазон чисел що можуть бути представлені в римській системі, якщо конкретно - то цілі від 1 до 3999. Римляни мали кілька способів запису більших чисел, наприклад поміщуючи вертикальну риску над числом щоб позначити те що його звичайне значення слід домножити на 1000. Та протягом цього розділу будемо вважати що римські числа простягаються лише від 1 до 3999. Не існує способу представити 0 в римській системі. Також римляни не вміли записувати від'ємні числа. Як і частки дробових чисел.
Давайте почнемо окреслювати те що повинен робити модуль roman.py. Він матиме дві головні функції, to_roman() та from_roman(). Функція to_roman() повинна отримати на вхід ціле число в діапазоні 1..3999 і повернути рядок з записом числа в римській системі...
Зупиніться прямо тут. Тепер давайте зробимо дещо неочікуване: напишемо тест який пеервіряє що функція to_roman() робить те що ми хочемо. Ви це правильно прочитали, ми збираємось написати код що тестує код який ми ще навіть не написали.
Це називається розробкою керованою тестами (), чи TDD. Набір двох функцій перетворення - to_roman() та from_roman(), можна написати і тестувати як модуль, окремо від будь-якої більшої програми що їх використовує, і в такому випадку тести називаються модульними. Python має фреймворк для модульного тестування - відповідно названий модуль unittest.
Модульне тестування є важливою частиною стратегії розробки що фокусується на тестуванні. Коли ви пишете модульні тести, важливо писати їх рано, і постійно їх оновлювати протягом того як код та вимоги до нього змінюються. Багато людей пропагують написання тестів перед написанням коду який тестуєтьс, і це підхід який я збираюсь продемонструвати в цьому розділі. Але модульні тести корисні не залежно від того коли ви їх напишете.
Написання тестів перед написанням коду змушує вас деталізувати вимоги в досить корисній формі. Написання тестів перед написанням коду запобігає надмірній розробці. Якщо всі тести проходяться значить функцію завершено. При рефакторингу вони допомагають довести що нова версія поводиться так само як і стара. При підтримці коду тести допомагають прикрити власну задницю коли хтось підходить з криком що ваша остання зміна поламала роботу їх старого коду. ("Але пане, всі тести проходяться, я перевірив..") При написанні коду в команді, підтримка вичерпного набору тестів на порядок зменшує шанси того що ваш код поламає якийсь інший код, тому що ви можете спершу запустити їхні модульні тести. (Я спостерігав такі речі під час спринтів розробки. Команда розбиває завдання, кожен бере специфікацію для своєї частини, пише тести для неї, потім ділиться своїми тестами з рештою команди. Таким чином, ніхто не заходить занадто далеко в розробці коду який не надто добре працює з кодом інших).
Єдине питання Кожен тест - це острів.
Тест відповідає на єдине запитання про код що тестується. Тест повинен бути здатним...
... працювати цілком самостійно, без жодного людського вводу. Модульне тестування - це автоматизація. ... самостійно визначати чи функція що він тестує пройшла тест чи його завалила, без людини що інтерпретує результат. ... запускатись повністю окремо від інших тестів (навіть якщо вони тестують ту саму функцію). Кожен тест - це острів.
Знаючи це, давайте створимо тест для першої вимоги:
Функція to_roman() повинна повертати римський запис будь-якого цілого числа від 1 до 3999.
Зразу не дуже ясно як код нижче робить..., ну хоч що-небудь. Він описує клас без методу __init__(). Клас має інший метод, але він ніде не викликається. Скрипт має блок __main__, але цей блок не звертається до класу чи його методу. Але дещо він робить, я обіцяю.
import roman1 import unittest
class KnownValues(unittest.TestCase): ① known_values = ( (1, 'I'), (2, 'II'), (3, 'III'), (4, 'IV'), (5, 'V'), (6, 'VI'), (7, 'VII'), (8, 'VIII'), (9, 'IX'), (10, 'X'), (50, 'L'), (100, 'C'), (500, 'D'), (1000, 'M'), (31, 'XXXI'), (148, 'CXLVIII'), (294, 'CCXCIV'), (312, 'CCCXII'), (421, 'CDXXI'), (528, 'DXXVIII'), (621, 'DCXXI'), (782, 'DCCLXXXII'), (870, 'DCCCLXX'), (941, 'CMXLI'), (1043, 'MXLIII'), (1110, 'MCX'), (1226, 'MCCXXVI'), (1301, 'MCCCI'), (1485, 'MCDLXXXV'), (1509, 'MDIX'), (1607, 'MDCVII'), (1754, 'MDCCLIV'), (1832, 'MDCCCXXXII'), (1993, 'MCMXCIII'), (2074, 'MMLXXIV'), (2152, 'MMCLII'), (2212, 'MMCCXII'), (2343, 'MMCCCXLIII'), (2499, 'MMCDXCIX'), (2574, 'MMDLXXIV'), (2646, 'MMDCXLVI'), (2723, 'MMDCCXXIII'), (2892, 'MMDCCCXCII'), (2975, 'MMCMLXXV'), (3051, 'MMMLI'), (3185, 'MMMCLXXXV'), (3250, 'MMMCCL'), (3313, 'MMMCCCXIII'), (3408, 'MMMCDVIII'), (3501, 'MMMDI'), (3610, 'MMMDCX'), (3743, 'MMMDCCXLIII'), (3844, 'MMMDCCCXLIV'), (3888, 'MMMDCCCLXXXVIII'), (3940, 'MMMCMXL'), (3999, 'MMMCMXCIX')) ②
def test_to_roman_known_values(self): ③ '''to_roman should give known result with known input''' for integer, numeral in self.known_values: result = roman1.to_roman(integer) ④ self.assertEqual(numeral, result) ⑤
if __name__ == '__main__': unittest.main()
① Щоб створити тест, спочатку створіть підклас класу TestCase з модуля unittest. Цей клас надає багато корисних методів які можна використовувати в тестах для перевірки різноманітних умов.
② Це кортеж з пар число/його римський запис, які я перевірив вручну. Він включає десять найменших чисел, найбільше число, кожне римське число що записується одним символом, і випадковий набір інших чисел. Не потрібно тестувати будь-який можливий випадок, але потрібно намагатись протестувати всі очевидні крайні випадки.
③ Кожен тест - це окремий метод. Метод тесту не приймає параметрів, не повертає даних, і повинен мати ім'я що починається з чотирьох символів: test. Якщо виклик методу завершується без винятків, тест вважається пройденим, якщо метод кидає виняток, тест вважається проваленим.
④ Ось тут ми робимо виклик до справжньої функції to_roman(). (Ну, функція все ще не написана, але як тільки вона буде, цей рядок її буде викликати.) Зауважте, що зараз ви описали API функції to_roman(): вона повинна приймати ціле значення (число для перетворення) і повертати рядок (його римський запис). Якщо API буде іншим, тест провалиться. Також зауважте що ми не ловимо жодних винятків при виклику to_roman(). Це навмисне. to_roman не повинен генерувати виняток коли йому передають дозволені значення, а всі перелічені значення дозволені. Якщо to_roman() згенерує виняток тест також провалиться.
⑤ Вважаючи що функція to_roman() була описана вірно, викликана вірно, завершилась успішно, і повернула значення, останнім кроком є перевірка того що повернене значення правильне. Це типова перевірка, і клас TestCase надає метод, assertEqual, для перевірки рівності двох значень. Якщо результат повернений з to_roman() не співпадає з очікуваним значенням, assertEqual, згенерує виняток і тест провалиться. Якщо два значення рівні, assertEqual нічого не зробить. Якщо кожне значення повернуте функцією to_roman() співпадає з очікуваним, assertEqual ніколи не згенерує виняток, тому test_to_roman_known_values з часом завершиться нормально, що означатиме що to_roman() пройшла тест.
Напишіть тест який валиться, тоді пишіть код поки тест не буде пройдено.
Після того як ви написали тест, можна починати кодити функцію to_roman(). Спершу, ви повинні створити заглушку з порожньої функції, та переконатись що тест валиться. Якщо тест успішний до того як ви написати код, ваші тести взагалі не тестують код! Модульне тестування - як танці: тести ведуть, код слідує. Напишіть тест який валиться, тоді програмуйте поки тест не буде пройдено.
# roman1.py
def to_roman(n): '''convert integer to Roman numeral''' pass
На даному етапі, ми хочемо описати сигнатуру функції to_roman(), але ми ще не хочемо писати її код. (Спершу потрібно щоб тест запустився). Щоб створити заглушку використаємо ключове слово pass, яке вказує на те, що нічого не потрібно робити.
Виконайте romantest1.py в командному рядку щоб запустити тест. Якщо передати при виклику опцію -v, ми отримаємо більш детальний вивід, і зможемо побачити що відбувається при запуску кожного тесту. Наш вивід виглядатиме приблизно так, навіть якщо пощастить:
you@localhost:~/diveintopython3/examples$ python3 romantest1.py -v test_to_roman_known_values (__main__.KnownValues) ① to_roman should give known result with known input ... FAIL ②
FAIL: to_roman should give known result with known input
Traceback (most recent call last): File "romantest1.py", line 73, in test_to_roman_known_values self.assertEqual(numeral, result) AssertionError: 'I' != None ③
Ran 1 test in 0.016s ④
FAILED (failures=1) ⑤
① Запуск скрипта запускає unittest.main(), який виконує кожен тест. Кожен тестовий приклад є окремим методом класу. Обов'язкового формату організації цих класів немає, кожен з них може містити єдиний тестовий метод, чи ви можете мати один клас що містить багато методів. Єдина вимога - кожен клас повинен наслідуватись від unittest.TestCase.
② Для кожного тестового методу модуль unittest роздрукує його рядок документації та результат проходження тесту. Як і очікувалось наш тест валиться.
③ Для кожного провального тесту, unittest виводить трейсбек що показує що саме трапилось. В даному випадку, виклик assertEqual() згенерував AssertionError тому що очікувалось що to_roman(1) поверне 'I', а вона не повернула. (Так як явного виклику return не було, функція повернула None - позначення "нічого" в Python).
④ Після детального опису кожного тесту, unittest показує підсумок того скільки тестів було виконано, і скільки часу це зайняло.
⑤ Загалом, тестування провалено, тому що принаймі один з тестів не пройдено. Коли тест валиться, unittest відрізняє провали та помилки. Провал - це виклик методів assertXYZ, таких як assertEqual чи assertRaises, які провалюютьсяч тому що умова не виконана, наприклад виняток не згенерований. Помилка - будь-який інший виняток згенерований з коду що тестується, чи самого методу що тестує.
Ну, і нарешті, ми можемо написати функцію to_roman().
roman_numeral_map = (('M', 1000), ('CM', 900), ('D', 500), ('CD', 400), ('C', 100), ('XC', 90), ('L', 50), ('XL', 40), ('X', 10), ('IX', 9), ('V', 5), ('IV', 4), ('I', 1)) ①
def to_roman(n): '''convert integer to Roman numeral''' result = '' for numeral, integer in roman_numeral_map: while n >= integer: ② result += numeral n -= integer return result
① roman_numeral_map - кортеж з кортежів який описує три речі: символьне представлення базових римських чисел, їх порядок (спадний, від M аж до I), та значення кожного числа. Кожен внутрішній кортеж - це пара (numeral, value). Це не тільки односимвольні римські числа, тут також описуються двосимвольні пари на зразок CM (“на сотню менше ніж тисяча”). Це дозволяє спростити код функції to_roman().