Nacházíte se zde: Domů ‣ Ponořme se do Pythonu 3 ‣
Úroveň obtížnosti: ♦♦♢♢♢
❝ Certitude is not the test of certainty. We have been cocksure of many things that were not so. ❞
(Pocit jistoty není měřítkem jistoty. Byli jsme si skálopevně jisti mnoha věcmi, které takové nebyly.)
— Oliver Wendell Holmes, Jr.
Ta dnešní mládež. Jsou tak zkažení těmi rychlými počítači a módními „dynamickými“ jazyky. Rychle napsat, pak dodat a ladit až nakonec (jestli vůbec). Za mých časů jsme dodržovali disciplínu. Říkám disciplínu! Museli jsme psát programy ručně, na papír a cpát je do počítače na děrných štítcích. A ono se nám to líbilo! A cože? Že je ten nadpis anglicky? Buďte rádi, že není v ruštině. Mnozí z vás ani neví, jak přečíst jednotlivá písmenka azbuky. No dobrá, trochu zvážním. Dá se to přeložit jako „testování jednotek“ nebo „jednotkové testování“. Ještě se k tomu dostaneme.
V této kapitole si napíšeme a odladíme pár pomocných funkcí pro konverzi na a z římských čísel. Způsob tvorby a ověřování římských čísel jsme si ukázali v podkapitole Případová studie: Římská čísla. Teď si poodstoupíme a zvážíme, kolik by dalo práce rozšířit původní kód na obousměrné pomocné funkce.
Pravidla pro římská čísla vedla k řadě zajímavých postřehů:
1
do 3999
. Římané používali několik způsobů vyjádření větších čísel. Tak například pruhem nad římským číslem vyjadřovali, že jeho číselná hodnota musí být vynásobená tisícem. Pro účely této kapitoly budeme uvažovat jen římská čísla od 1
do 3999
.
Začněme mapovat, co by takový modul roman.py
měl dělat. Bude obsahovat dvě hlavní funkce, to_roman()
(na římské číslo) a from_roman()
(z římského čísla). Funkce to_roman()
by měla převzít celé číslo v intervalu od 1
do 3999
a vrátit jeho reprezentaci římskými číslicemi jako řetězec…
Hned tady se zastavíme. Teď uděláme něco trošku neočekávaného. Napíšeme si testovací příklad, který kontroluje, zda funkce to_roman()
dělá to, co po ní chceme. Čtete dobře. Jdeme psát kód, který testuje jiný kód, který jsme ještě nenapsali.
Říká se tomu vývoj řízený testy (test-driven development) nebo TDD. (V anglické literatuře si potrpí na zavádění a používání zkratek.) Dvojice převodních funkcí — to_roman()
a později from_roman()
— může být napsána a testována jako jednotka (unit), odděleně od jakéhokoliv většího programu, který funkce importuje. V Pythonu najdeme rámec (framework) pro unit testing (tedy testování jednotek), který má podobu příhodně nazvaného modulu unittest
.
Unit testing (testování jednotek) představuje důležitou součást celkové vývojové strategie založené na testování. Pokud testy jednotek píšete, je důležité, abyste je napsali brzy a abyste je udržovali v závislosti na změnách kódu a požadavků. Mnozí lidé se přimlouvají za to, aby se testy psaly dříve než kód, který mají testovat. V této kapitole si takový přístup předvedeme. Ale testy jednotek mají své výhody nezávisle na tom, kdy je napíšete.
⁂
Testovací případ (test case) odpovídá na jedinou otázku, která se testovaného kódu týká. Testovací případ by měl být schopen…
S ohledem na uvedené předpoklady začněme budovat testovací případ pro první požadavek:
to_roman()
by měla vracet reprezentaci římského čísla pro všechna celá čísla v intervalu 1
až 3999
.
V prvním okamžiku není zřejmé, jak následující kód dělá… no vlastně cokoliv. Definuje třídu, která nemá žádnou metodu __init__()
. Třída sice má nějakou metodu, ale ta se nikdy nevolá. Celý skript obsahuje blok __main__
, ale nenajdeme v něm odkaz ani na třídu, ani na její metodu. Ale on opravdu něco dělá. Za to ručím.
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
z modulu unittest
. Uvedená třída nám poskytuje řadu užitečných metod, které můžeme v našem testovacím případě využít pro testování specifických podmínek.
test
. Pokud testovací metoda skončí normálně, bez vyvolání výjimky, pokládáme test za úspěšný. Pokud metoda vyvolá výjimku, považujeme to za selhání testu.
to_roman()
. (Tu funkci jsme zatím nenapsali, ale jakmile ji jednou napíšeme, tento řádek ji zavolá.) Všimněte si, že jsme v tomto okamžiku pro funkci to_roman()
definovali aplikační programové rozhraní (API). Musí přebírat celé číslo (převáděné číslo) a vrací řetězec (reprezentaci římského čísla). Pokud by rozhraní funkce bylo jiné, test by selhal. Všimněte si také, že při volání to_roman()
žádnou výjimku neodchytáváme. Je to záměrné. Funkce to_roman()
by při volání s platným vstupem žádnou výjimku vyvolat neměla a uvedené vstupní hodnoty jsou všechny platné. Pokud to_roman()
vyvolá výjimku, bude se to považovat za selhání tohoto testu.
to_roman()
byla korektně definována, korektně volána, úspěšně skončila a vrátila výsledek. Pak nám jako poslední krok zbývá zkontrolovat, zda vrátila správnou hodnotu. Jde o obecně používaný dotaz. Ke kontrole, zda se dvě hodnoty shodují, poskytuje třída TestCase
metodu assertEqual
. Pokud výsledek (result) vrácený funkcí to_roman()
neodpovídá očekávané známé hodnotě (numeral), vyvolá assertEqual
výjimku a test selže. Pokud se ty dvě hodnoty shodují, neudělá assertEqual
nic. Pokud všechny hodnoty vrácené funkcí to_roman()
odpovídají očekávaným hodnotám, assertEqual
nikdy výjimku nevyvolá, takže metoda test_to_roman_known_values
nakonec normálně skončí. To znamená, že funkce to_roman()
testem prošla.
Jakmile máme vytvořen testovací případ, začneme psát funkci to_roman()
. Nejdříve ji nahradíme prázdnou funkcí a ověříme si, že test selhává. Pokud by test prošel, aniž jsme napsali nějaký kód, pak by testy náš kód vůbec netestovaly! Unit testing je jako tanec: testy vedou, kód následuje. Napište test, který selže, a pak programujte, dokud neprojde.
# roman1.py
def to_roman(n):
'''convert integer to Roman numeral'''
pass ①
to_roman()
, ale nechceme zatím psát žádný kód. (Náš test musí nejdříve selhat.) Prázdné funkčnosti dosáhneme použitím pythonovského vyhrazeného slova pass
, které dělá doslova nic.
Spuštění testu zajistíme provedením romantest1.py
z příkazového řádku. Pokud jej zavoláme s volbou -v
, dosáhneme podrobnějšího výstupu, takže přesně uvidíme, co se při běhu každého testovacího případu děje. S trochou štěstí by váš výstup měl vypadat nějak takto:
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()
, která zajistí provedení každého testovacího případu. Každý testovací případ je metodou třídy z romantest1.py
. U testovacích tříd se nevyžaduje nějaká zvláštní organizace. Každá z nich může obsahovat jedinou metodu, nebo můžeme mít jednu třídu, která obsahuje množství testovacích metod. Jediným požadavkem je to, že každá testovací třída musí dědit z třídy unittest.TestCase
.
unittest
vytiskne docstring
metody a to, zda test prošel (pass) nebo selhal (fail). Tento test podle očekávání selhal.
unittest
trasovací informaci, která přesně ukazuje, co se stalo. V tomto případě vyvolala metoda assertEqual()
výjimku AssertionError
, protože se očekávalo, že funkce to_roman(1)
vrátí 'I'
, ale nevrátila. (Protože jsme v ní explicitně neuvedli příkaz return
, vrátila funkce hodnotu None
, což je pythonovský ekvivalent hodnoty null.)
unittest
souhrnně, kolik testů se provádělo a jak dlouho to trvalo.
unittest
mezi selháním (failure) a chybou (error). Selhání (failure) je důsledkem volání metody assertXYZ
, jako je například assertEqual
nebo assertRaises
, která selhala, protože neplatí předepsaná podmínka nebo nebyla vyvolána očekávaná výjimka. Za chybu (error) se považuje jakýkoliv jiný druh výjimky, která vznikla uvnitř testované kódu nebo v kódu testovacího případu.
A teď už můžeme konečně napsat funkci 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
M
až po I
), hodnotu každého římského čísla. Každá vnitřní n-tice je dvojicí (římské číslo, hodnota)
. Nejsou zde jen jednoznaková římská čísla. Jsou zde definována i dvojznaková čísla jako CM
(„o jedno sto méně než jeden tisíc“). Tím se kód funkce to_roman()
zjednoduší.
Pokud vám pořád není jasné, jak funkce to_roman()
pracuje, přidejte na konec cyklu while
volání funkce print()
:
while n >= integer:
result += numeral
n -= integer
print('subtracting {0} from input, adding {1} to output'.format(integer, numeral))
S ladicími příkazy print()
vypadá výstup takto:
>>> import roman1 >>> roman1.to_roman(1424) subtracting 1000 from input, adding M to output subtracting 400 from input, adding CD to output subtracting 10 from input, adding X to output subtracting 10 from input, adding X to output subtracting 4 from input, adding IV to output 'MCDXXIV'
Takže se zdá, že funkce to_roman()
pracuje přinejmenším v tomto ručně zkoušeném případě. Ale projde testovacím případem, který jsme napsali?
you@localhost:~/diveintopython3/examples$ python3 romantest1.py -v
test_to_roman_known_values (__main__.KnownValues)
to_roman should give known result with known input ... ok ①
----------------------------------------------------------------------
Ran 1 test in 0.016s
OK
to_roman()
prošla testovacím případem nazvaným „známé hodnoty“. Není sice všeobsažný, ale prověřil schopnosti funkce celou škálou vstupů, včetně vstupů, které produkují každé jednoznakové římské číslo, největší možný vstup (3999
), a vstupu, který produkuje nejdelší možné římské číslo (3888
). V tomto okamžiku už můžeme docela důvěřovat tomu, že funkce pracuje pro libovolnou správnou vstupní hodnotu, kterou bychom mohli zadat.
„Správný“ vstup? Hmm. A co takhle chybný vstup?
⁂
Ono ale nestačí, když funkce uspějí při zadání správného vstupu. Musíme otestovat také to, že při chybném vstupu dojde k jejich selhání. Ale nemůže jít o jakýkoliv způsob selhání. Funkce musí selhat očekávaným způsobem.
>>> import roman1
>>> roman1.to_roman(4000)
'MMMM'
>>> roman1.to_roman(5000)
'MMMMM'
>>> roman1.to_roman(9000) ①
'MMMMMMMMM'
Měli byste si položit otázku: „Jak bychom to mohli vyjádřit formou testovatelného požadavku?“ Co kdybychom začali nějak takto:
Pokud funkci
to_roman()
zadáme celé číslo větší než3999
, měla by vyvolat výjimkuOutOfRangeError
.
Jak by vypadal příslušný test?
import unittest, roman2
class ToRomanBadInput(unittest.TestCase): ①
def test_too_large(self): ②
'''to_roman should fail with large input'''
self.assertRaises(roman2.OutOfRangeError, roman2.to_roman, 4000) ③
unittest.TestCase
. Jedna třída sice může obsahovat více než jeden test (jak si ukážeme v této kapitole později), ale já jsem se rozhodl, že vytvořím novou třídu, protože tento test dělá něco jiného než ten minulý. Všechny testy správných vstupů budeme udržovat v jedné třídě a o všechny testy chybných vstupů se bude starat druhá třída.
test
.
unittest.TestCase
poskytuje metodu assertRaises
, která přebírá následující argumenty: očekávanou výjimku, testovanou funkci a argumenty, které jí chceme předat. (Pokud testovaná funkce očekává více než jeden argument, předejte je metodě assertRaises
všechny v daném pořadí. Ona už se postará o jejich předání testované funkci.)
Věnujte zvláštní pozornost tomu poslednímu řádku kódu. Místo toho, abychom volali to_roman()
, přímo a ručně zkontrolovali, že vyvolává konkrétní výjimku (obalením do bloku try...except
), metoda assertRaises
to vše udělá za nás. Musíme jí jen říct, jakou výjimku očekáváme (roman2.OutOfRangeError
), předat funkci (to_roman()
) a její argumenty (4000
). Metoda assertRaises
se postará o zavolání to_roman()
a o kontrolu toho, že vyvolala výjimku roman2.OutOfRangeError
.
Poznamenejme také, že funkci to_roman()
předáváme jako argument. Nevoláme ji a ani nepředáváme její jméno jako řetězec. Zmínil jsem se už dříve o tom, jak je šikovné, že v Pythonu je vše objektem?
Takže co se stane, když spustíme sadu testů doplněnou o tento nový test?
you@localhost:~/diveintopython3/examples$ python3 romantest2.py -v test_to_roman_known_values (__main__.KnownValues) to_roman should give known result with known input ... ok test_too_large (__main__.ToRomanBadInput) to_roman should fail with large input ... ERROR ① ====================================================================== ERROR: to_roman should fail with large input ---------------------------------------------------------------------- Traceback (most recent call last): File "romantest2.py", line 78, in test_too_large self.assertRaises(roman2.OutOfRangeError, roman2.to_roman, 4000) AttributeError: 'module' object has no attribute 'OutOfRangeError' ② ---------------------------------------------------------------------- Ran 2 tests in 0.000s FAILED (errors=1)
OutOfRangeError
(tj. hodnota mimo platný rozsah). Připomeňme si, že uvedenou výjimku jsme předali metodě assertRaises()
, protože právě tohle má být výjimka, kterou má funkce vyvolat, když zadáme vstup mimo platný rozsah. Ale tato výjimka vůbec neexistuje, takže volání metody assertRaises()
selhalo. Metoda neměla vůbec šanci otestovat funkci to_roman()
. Tak daleko se vůbec nedostala.
K vyřešení zmíněného problému musíme v roman2.py
doplnit definici výjimky OutOfRangeError
.
class OutOfRangeError(ValueError): ①
pass ②
ValueError
. Není to nezbytně nutné (mohli bychom prostě dědit od bázové třídy Exception
, tj. obecná výjimka), ale zdá se to být správné.
pass
sice nic nedělá, ale je to řádek pythonovského kódu, který zajistí, že třída vznikne.
Teď spustíme sadu testů znovu.
you@localhost:~/diveintopython3/examples$ python3 romantest2.py -v test_to_roman_known_values (__main__.KnownValues) to_roman should give known result with known input ... ok test_too_large (__main__.ToRomanBadInput) to_roman should fail with large input ... FAIL ① ====================================================================== FAIL: to_roman should fail with large input ---------------------------------------------------------------------- Traceback (most recent call last): File "romantest2.py", line 78, in test_too_large self.assertRaises(roman2.OutOfRangeError, roman2.to_roman, 4000) AssertionError: OutOfRangeError not raised by to_roman ② ---------------------------------------------------------------------- Ran 2 tests in 0.016s FAILED (failures=1)
assertRaises()
tentokrát prošlo a rámec pro testování jednotek (unit test framework) skutečně testoval funkci to_roman()
.
to_roman()
zatím, samozřejmě, nevyvolává právě definovanou výjimku OutOfRangeError
, protože jsme jí ještě neřekli, že to má dělat. To je ale výborná zpráva! Znamená to, že máme platný testovací případ — selhává (fails) před napsáním kódu, který zajistí, že projde.
Teď napíšeme kód, který zajistí, aby funkce testem prošla.
def to_roman(n):
'''convert integer to Roman numeral'''
if n > 3999:
raise OutOfRangeError('number out of range (must be less than 4000)') ①
result = ''
for numeral, integer in roman_numeral_map:
while n >= integer:
result += numeral
n -= integer
return result
3999
, vyvolej výjimku OutOfRangeError
. Tento jednotkový test nekontroluje, zda výjimku doprovází lidsky čitelný řetězec. Mohli bychom napsat další test, který by to kontroloval (ale pozor na problémy s internacionalizací; řetězce se mohou lišit v závislosti na jazyku uživatele a v závislosti na prostředí).
Vede úprava k tomu, že test projde? Pojďme to zjistit.
you@localhost:~/diveintopython3/examples$ python3 romantest2.py -v
test_to_roman_known_values (__main__.KnownValues)
to_roman should give known result with known input ... ok
test_too_large (__main__.ToRomanBadInput)
to_roman should fail with large input ... ok ①
----------------------------------------------------------------------
Ran 2 tests in 0.000s
OK
⁂
Spolu s testováním čísel, která jsou příliš velká, bychom měli testovat i čísla, která jsou příliš malá. Přesně jak jsme poznamenali v našich požadavcích na funkčnost, římská čísla nemohou vyjádřit nulu nebo záporná čísla.
>>> import roman2 >>> roman2.to_roman(0) '' >>> roman2.to_roman(-1) ''
Hmm, tohle není dobré. Přidejme testy pro každou z těchto podmínek.
class ToRomanBadInput(unittest.TestCase):
def test_too_large(self):
'''to_roman should fail with large input'''
self.assertRaises(roman3.OutOfRangeError, roman3.to_roman, 4000) ①
def test_zero(self):
'''to_roman should fail with 0 input'''
self.assertRaises(roman3.OutOfRangeError, roman3.to_roman, 0) ②
def test_negative(self):
'''to_roman should fail with negative input'''
self.assertRaises(roman3.OutOfRangeError, roman3.to_roman, -1) ③
test_too_large()
se od minulého kroku nezměnila. Ponechal jsem ji zde, abych ukázal, kam nový kód zapadá.
test_zero()
. Je to stejné jako u metody test_too_large()
. Metodě assertRaises()
z třídy unittest.TestCase
říkáme, aby zavolala naši funkci to_roman()
s parametrem 0
a zkontrolovala, zda vyvolá příslušnou výjimku OutOfRangeError
.
test_negative()
je téměř shodná až na to, že funkci to_roman()
předává hodnotu -1
. Pokud kterýkoliv z těchto nových testů nevyvolá výjimku OutOfRangeError
(protože funkce buď vrátí nějakou skutečnou hodnotu nebo vyvolá nějakou jinou výjimku), bude se to považovat za selhání testu.
Teď zkontrolujme, že testy selhávají:
you@localhost:~/diveintopython3/examples$ python3 romantest3.py -v test_to_roman_known_values (__main__.KnownValues) to_roman should give known result with known input ... ok test_negative (__main__.ToRomanBadInput) to_roman should fail with negative input ... FAIL test_too_large (__main__.ToRomanBadInput) to_roman should fail with large input ... ok test_zero (__main__.ToRomanBadInput) to_roman should fail with 0 input ... FAIL ====================================================================== FAIL: to_roman should fail with negative input ---------------------------------------------------------------------- Traceback (most recent call last): File "romantest3.py", line 86, in test_negative self.assertRaises(roman3.OutOfRangeError, roman3.to_roman, -1) AssertionError: OutOfRangeError not raised by to_roman ====================================================================== FAIL: to_roman should fail with 0 input ---------------------------------------------------------------------- Traceback (most recent call last): File "romantest3.py", line 82, in test_zero self.assertRaises(roman3.OutOfRangeError, roman3.to_roman, 0) AssertionError: OutOfRangeError not raised by to_roman ---------------------------------------------------------------------- Ran 4 tests in 0.000s FAILED (failures=2)
Výborně. Oba testy podle očekávání selhaly. Teď se přepněme na psaní kódu a uvidíme, co můžeme dělat, aby testy prošly.
def to_roman(n):
'''convert integer to Roman numeral'''
if not (0 < n < 4000): ①
raise OutOfRangeError('number out of range (must be 1..3999)') ②
result = ''
for numeral, integer in roman_numeral_map:
while n >= integer:
result += numeral
n -= integer
return result
if not ((0 < n) and (n < 4000))
, ale je to mnohem čitelnější. Tento řádek kódu by měl zachytit vstupy, které jsou příliš velké, záporné nebo nulové.
unittest
je to jedno. Pokud by ale váš kód vyvolával nesprávně popsané výjimky, ztížilo by se tím ruční ladění.
Mohl bych vám ukázat celou sérii nesouvisejících příkladů, které ukazují, že zkratka umožňující několik porovnání najednou funguje. Místo toho ale spustím testy jednotek a dokážu vám to.
you@localhost:~/diveintopython3/examples$ python3 romantest3.py -v test_to_roman_known_values (__main__.KnownValues) to_roman should give known result with known input ... ok test_negative (__main__.ToRomanBadInput) to_roman should fail with negative input ... ok test_too_large (__main__.ToRomanBadInput) to_roman should fail with large input ... ok test_zero (__main__.ToRomanBadInput) to_roman should fail with 0 input ... ok ---------------------------------------------------------------------- Ran 4 tests in 0.016s OK
⁂
Mezi požadavky na převod na římská čísla byl ještě jeden, který se týkal neceločíselného vstupu.
>>> import roman3 >>> roman3.to_roman(0.5) ① '' >>> roman3.to_roman(1.0) ② 'I'
Testování na neceločíselný vstup není obtížné. Nejdříve si definujeme výjimku NotIntegerError
.
# roman4.py
class OutOfRangeError(ValueError): pass
class NotIntegerError(ValueError): pass
Dále napíšeme testovací případ, který kontroluje výskyt výjimky NotIntegerError
.
class ToRomanBadInput(unittest.TestCase):
.
.
.
def test_non_integer(self):
'''to_roman should fail with non-integer input'''
self.assertRaises(roman4.NotIntegerError, roman4.to_roman, 0.5)
Teď zkontrolujme, zda test správně selhává.
you@localhost:~/diveintopython3/examples$ python3 romantest4.py -v test_to_roman_known_values (__main__.KnownValues) to_roman should give known result with known input ... ok test_negative (__main__.ToRomanBadInput) to_roman should fail with negative input ... ok test_non_integer (__main__.ToRomanBadInput) to_roman should fail with non-integer input ... FAIL test_too_large (__main__.ToRomanBadInput) to_roman should fail with large input ... ok test_zero (__main__.ToRomanBadInput) to_roman should fail with 0 input ... ok ====================================================================== FAIL: to_roman should fail with non-integer input ---------------------------------------------------------------------- Traceback (most recent call last): File "romantest4.py", line 90, in test_non_integer self.assertRaises(roman4.NotIntegerError, roman4.to_roman, 0.5) AssertionError: NotIntegerError not raised by to_roman ---------------------------------------------------------------------- Ran 5 tests in 0.000s FAILED (failures=1)
Napíšeme kód, který má zajistit, aby test prošel.
def to_roman(n):
'''convert integer to Roman numeral'''
if not (0 < n < 4000):
raise OutOfRangeError('number out of range (must be 1..3999)')
if not isinstance(n, int): ①
raise NotIntegerError('non-integers can not be converted') ②
result = ''
for numeral, integer in roman_numeral_map:
while n >= integer:
result += numeral
n -= integer
return result
isinstance()
testuje, zda je daná proměnná určitého typu (nebo, z technického hlediska, nějakého z něj odvozeného typu).
int
, vyvolej naši zbrusu novou výjimku NotIntegerError
.
Nakonec zkontrolujeme, že tento kód zajistil průchod testem.
you@localhost:~/diveintopython3/examples$ python3 romantest4.py -v test_to_roman_known_values (__main__.KnownValues) to_roman should give known result with known input ... ok test_negative (__main__.ToRomanBadInput) to_roman should fail with negative input ... ok test_non_integer (__main__.ToRomanBadInput) to_roman should fail with non-integer input ... ok test_too_large (__main__.ToRomanBadInput) to_roman should fail with large input ... ok test_zero (__main__.ToRomanBadInput) to_roman should fail with 0 input ... ok ---------------------------------------------------------------------- Ran 5 tests in 0.000s OK
Funkce to_roman()
prošla všemi testy a žádné další testy mě nenapadají. Takže nastal čas, abychom se přesunuli k from_roman()
.
⁂
Převod řetězce vyjadřujícího římské číslo na číselnou hodnotu vypadá složitěji než převod čísla na římské číslo. Určitě budeme muset zajistit ověření platnosti. Zkontrolovat, zda je číslo rovno nule, je snadné. O něco obtížněji se kontroluje, zda je řetězec platným římským číslem. Jenže my už jsme zkonstruovali regulární výraz, který zkontroluje, zda jde o římské číslo. Takže tuhle část už máme hotovou.
Zbývá nám problém samotné konverze řetězce. Jak za chvíli uvidíme, díky existenci datové struktury, kterou jsme definovali pro převod určitých římských čísel na celočíselné hodnoty, bude jádro funkce from_roman()
stejně přímočaré jako u funkce to_roman()
.
Ale nejdříve testy. Pro ověření správnosti konkrétních hodnot budeme potřebovat test „známých hodnot“. Naše testovací sada již tabulku známých hodnot obsahuje, takže ji využijme.
def test_from_roman_known_values(self):
'''from_roman should give known result with known input'''
for integer, numeral in self.known_values:
result = roman5.from_roman(numeral)
self.assertEqual(integer, result)
Najdeme zde potěšitelnou symetrii. Funkce to_roman()
a from_roman()
jsou vzájemně inverzní. První z nich převádí čísla na zvláštně formátované řetězce a druhá převádí zvláštně formátované řetězce na celá čísla. Teoreticky bychom měli být schopni dospět ke zvolenému číslu oklikou tak, že je nejdříve předáme funkci to_roman()
. Získaný řetězec předáme funkci from_roman()
a výsledné číslo by se mělo shodovat s počátečním.
n = from_roman(to_roman(n)) pro všechny hodnoty n
V tomto případě „všechny hodnoty“ znamená jakoukoliv hodnotu 1..3999
, protože toto je platný rozsah vstupů pro funkci to_roman()
. Tuto symetrii můžeme vyjádřit testovacím případem, který prochází všechny hodnoty 1..3999
, volá to_roman()
, volá from_roman()
a kontroluje, zda se výstup shoduje s původním vstupem.
class RoundtripCheck(unittest.TestCase):
def test_roundtrip(self):
'''from_roman(to_roman(n))==n for all n'''
for integer in range(1, 4000):
numeral = roman5.to_roman(integer)
result = roman5.from_roman(numeral)
self.assertEqual(integer, result)
Tyto nové testy zatím ani neselžou (fail). Zatím jsme vůbec nedefinovali funkci from_roman()
, takže způsobí chyby (errors).
you@localhost:~/diveintopython3/examples$ python3 romantest5.py E.E.... ====================================================================== ERROR: test_from_roman_known_values (__main__.KnownValues) from_roman should give known result with known input ---------------------------------------------------------------------- Traceback (most recent call last): File "romantest5.py", line 78, in test_from_roman_known_values result = roman5.from_roman(numeral) AttributeError: 'module' object has no attribute 'from_roman' ====================================================================== ERROR: test_roundtrip (__main__.RoundtripCheck) from_roman(to_roman(n))==n for all n ---------------------------------------------------------------------- Traceback (most recent call last): File "romantest5.py", line 103, in test_roundtrip result = roman5.from_roman(numeral) AttributeError: 'module' object has no attribute 'from_roman' ---------------------------------------------------------------------- Ran 7 tests in 0.019s FAILED (errors=2)
Problém vyřešíme rychlým vytvořením náhradní funkce.
# roman5.py
def from_roman(s):
'''convert Roman numeral to integer'''
(Hej, všimli jste si toho? Definoval jsem funkci, která neobsahuje nic než docstring (dokumentační řetězec). Tohle je v Pythonu legální. Někteří programátoři vás ve skutečnosti zapřísahají: „Nepište náhrady. Dokumentujte!“)
Teď už testovací případy opravdu selžou (fail).
you@localhost:~/diveintopython3/examples$ python3 romantest5.py F.F.... ====================================================================== FAIL: test_from_roman_known_values (__main__.KnownValues) from_roman should give known result with known input ---------------------------------------------------------------------- Traceback (most recent call last): File "romantest5.py", line 79, in test_from_roman_known_values self.assertEqual(integer, result) AssertionError: 1 != None ====================================================================== FAIL: test_roundtrip (__main__.RoundtripCheck) from_roman(to_roman(n))==n for all n ---------------------------------------------------------------------- Traceback (most recent call last): File "romantest5.py", line 104, in test_roundtrip self.assertEqual(integer, result) AssertionError: 1 != None ---------------------------------------------------------------------- Ran 7 tests in 0.002s FAILED (failures=2)
Nastal čas napsat funkci from_roman()
.
def from_roman(s):
"""convert Roman numeral to integer"""
result = 0
index = 0
for numeral, integer in roman_numeral_map:
while s[index:index+len(numeral)] == numeral: ①
result += integer
index += len(numeral)
return result
to_roman()
. Procházíme datovou strukturou s římskými čísly (n-tice n-tic), ale místo hledání nejvyšších možných číselných hodnot se snažíme hledat řetězec znaků s „nejvyšším“ možným římským číslem.
Pokud vám pořád není jasné, jak funkce from_roman()
pracuje, přidejte na konec cyklu while
volání funkce print
:
def from_roman(s):
"""convert Roman numeral to integer"""
result = 0
index = 0
for numeral, integer in roman_numeral_map:
while s[index:index+len(numeral)] == numeral:
result += integer
index += len(numeral)
print('found', numeral, 'of length', len(numeral), ', adding', integer)
>>> import roman5 >>> roman5.from_roman('MCMLXXII') found M of length 1, adding 1000 found CM of length 2, adding 900 found L of length 1, adding 50 found X of length 1, adding 10 found X of length 1, adding 10 found I of length 1, adding 1 found I of length 1, adding 1 1972
Nastal opět čas ke spuštění testů.
you@localhost:~/diveintopython3/examples$ python3 romantest5.py ....... ---------------------------------------------------------------------- Ran 7 tests in 0.060s OK
Máme tady dvě vzrušující zprávy. Ta první je, že funkce from_roman()
funguje pro správné vstupy — přinejmenším pro všechny známé hodnoty. Ta druhá zpráva je, že test „kruhovým voláním“ (round trip test) také prošel. Když to zkombinujeme dohromady, můžeme si být docela jistí tím, že jak funkce to_roman()
, tak funkce from_roman()
pracují správně pro všechny možné správné hodnoty. (Není to ale zaručeno. Teoreticky je možné, že to_roman()
obsahuje chybu, která pro určité hodnoty vstupů produkuje špatná římská čísla, a současně funkce from_roman()
obsahuje obrácenou chybu, která produkuje stejná, ale špatná čísla přesně pro tu množinu římských čísel, která funkce to_roman()
vygenerovala nesprávně. V závislosti na vaší aplikaci a na požadavcích by vám to mohlo dělat starosti. Pokud tomu tak je, napište obsažnější testovací případy, které vaše starosti rozptýlí.)
⁂
Teď, když už funkce from_roman()
pracuje správně pro korektní vstup, nastal čas k umístění posledního kousku skládanky — zajištění správné funkce pro špatné vstupy. To znamená, že musíme najít způsob, jak se podívat na řetězec a určit, zda je platným římským číslem. To už je ze své podstaty obtížnější než ověřování správnosti číselného vstupu ve funkci to_roman()
. Ale máme k dispozici mocný nástroj — regulární výrazy. (Pokud regulární výrazy neznáte, pak je vhodná doba na to, abyste si přečetli kapitolu o regulárních výrazech.)
V podkapitole Případová studie: Římská čísla jsme viděli, že existuje několik jednoduchých pravidel pro konstrukci římského čísla, která jsou založena na využití písmen M
, D
, C
, L
, X
, V
a I
. Pojďme si tato pravidla zopakovat:
I
je 1
, II
je rovno 2
a III
znamená 3
. VI
se rovná 6
(doslova „5
a 1
“), VII
je 7
a VIII
je 8
.
I
, X
, C
a M
) se mohou opakovat nanejvýš třikrát. Hodnotu 4
musíme vyjádřit odečtením od dalšího vyššího pětkového znaku. Hodnotu 4
nemůžeme zapsat jako IIII
. Místo toho ji musíme zapsat jako IV
(„o 1
méně než 5
“). 40
se zapisuje jako XL
(„o 10
méně než 50
“), 41
jako XLI
, 42
jako XLII
, 43
jako XLIII
a následuje 44
jako XLIV
(„o 10
méně než 50
a k tomu o 1
méně než 5
“).
9
musíme vyjádřit odečtením od dalšího vyššího desítkového znaku: 8
zapíšeme jako VIII
, ale 9
zapíšeme IX
(„o 1
méně než 10
“) a ne jako VIIII
(protože znak I
nemůžeme opakovat čtyřikrát). 90
je XC
, 900
je CM
.
10
se vždy zapisuje jako X
a nikdy jako VV
. 100
je vždy C
, nikdy LL
.
DC
znamená 600
, ale CD
je úplně jiné číslo (400
, „o 100
méně než 500
“). CI
je 101
, ale IC
není dokonce vůbec platné římské číslo (protože 1
nemůžeme přímo odčítat od 100
; musíme to napsat jako XCIX
, „o 10
méně než 100
a k tomu o 1
méně než 10
“).
Takže jeden z užitečných testů bude ověřovat, že by funkce from_roman()
měla selhat (fail) v případě, kdy jí předáme řetězec s příliš mnoha opakujícími se římskými číslicemi. Co znamená „příliš mnoho“, závisí na konkrétní číslici.
class FromRomanBadInput(unittest.TestCase):
def test_too_many_repeated_numerals(self):
'''from_roman should fail with too many repeated numerals'''
for s in ('MMMM', 'DD', 'CCCC', 'LL', 'XXXX', 'VV', 'IIII'):
self.assertRaises(roman6.InvalidRomanNumeralError, roman6.from_roman, s)
Další užitečný test bude založen na kontrole, že se neopakují některé vzory. Například IX
je 9
, ale IXIX
je vždy neplatné.
def test_repeated_pairs(self):
'''from_roman should fail with repeated pairs of numerals'''
for s in ('CMCM', 'CDCD', 'XCXC', 'XLXL', 'IXIX', 'IVIV'):
self.assertRaises(roman6.InvalidRomanNumeralError, roman6.from_roman, s)
Třetí test by mohl kontrolovat, zda se číslice objevují ve správném pořadí, od nejvyšších k nejnižším hodnotám. Například CL
je 150
, ale LC
je vždy neplatné, protože číslice pro 50
se nesmí nikdy vyskytovat před číslicí pro 100
. Tento test zahrnuje náhodně zvolenou množinu nesprávných předchůdců: I
před M
, V
před X
a tak dále.
def test_malformed_antecedents(self):
'''from_roman should fail with malformed antecedents'''
for s in ('IIMXCC', 'VX', 'DCM', 'CMM', 'IXIV',
'MCMC', 'XCX', 'IVI', 'LM', 'LD', 'LC'):
self.assertRaises(roman6.InvalidRomanNumeralError, roman6.from_roman, s)
Každý z těchto testů spoléhá na to, že funkce from_roman()
vyvolává novou výjimku InvalidRomanNumeralError
, kterou jsme ještě nedefinovali.
# roman6.py
class InvalidRomanNumeralError(ValueError): pass
Všechny tři testy by měly selhat (fail), protože funkce from_roman()
momentálně neprovádí žádnou kontrolu platnosti. (Pokud by neselhaly teď, tak co by vlastně testovaly?)
you@localhost:~/diveintopython3/examples$ python3 romantest6.py FFF....... ====================================================================== FAIL: test_malformed_antecedents (__main__.FromRomanBadInput) from_roman should fail with malformed antecedents ---------------------------------------------------------------------- Traceback (most recent call last): File "romantest6.py", line 113, in test_malformed_antecedents self.assertRaises(roman6.InvalidRomanNumeralError, roman6.from_roman, s) AssertionError: InvalidRomanNumeralError not raised by from_roman ====================================================================== FAIL: test_repeated_pairs (__main__.FromRomanBadInput) from_roman should fail with repeated pairs of numerals ---------------------------------------------------------------------- Traceback (most recent call last): File "romantest6.py", line 107, in test_repeated_pairs self.assertRaises(roman6.InvalidRomanNumeralError, roman6.from_roman, s) AssertionError: InvalidRomanNumeralError not raised by from_roman ====================================================================== FAIL: test_too_many_repeated_numerals (__main__.FromRomanBadInput) from_roman should fail with too many repeated numerals ---------------------------------------------------------------------- Traceback (most recent call last): File "romantest6.py", line 102, in test_too_many_repeated_numerals self.assertRaises(roman6.InvalidRomanNumeralError, roman6.from_roman, s) AssertionError: InvalidRomanNumeralError not raised by from_roman ---------------------------------------------------------------------- Ran 10 tests in 0.058s FAILED (failures=3)
Fajn. Teď už do funkce from_roman()
potřebujeme přidat jen regulární výraz, který testuje platnost římských čísel.
roman_numeral_pattern = re.compile('''
^ # začátek řetězce
M{0,3} # tisíce - 0 až 3 M
(CM|CD|D?C{0,3}) # stovky - 900 (CM), 400 (CD), 0-300 (0 až 3 C),
# nebo 500-800 (D následované 0 až 3 C)
(XC|XL|L?X{0,3}) # desítky - 90 (XC), 40 (XL), 0-30 (0 až 3 X),
# nebo 50-80 (L následované 0 až 3 X)
(IX|IV|V?I{0,3}) # jednotky - 9 (IX), 4 (IV), 0-3 (0 až 3 I),
# nebo 5-8 (V následované 0 až 3 I)
$ # konec řetězce
''', re.VERBOSE)
def from_roman(s):
'''convert Roman numeral to integer'''
if not roman_numeral_pattern.search(s):
raise InvalidRomanNumeralError('Invalid Roman numeral: {0}'.format(s))
result = 0
index = 0
for numeral, integer in roman_numeral_map:
while s[index : index + len(numeral)] == numeral:
result += integer
index += len(numeral)
return result
A znovu spustíme testy…
you@localhost:~/diveintopython3/examples$ python3 romantest7.py .......... ---------------------------------------------------------------------- Ran 10 tests in 0.066s OK
A cenu za zklamání roku dostává… slovo „OK
“, které modul unittest
zobrazí poté, co všechny testy prošly.
© 2001–11 Mark Pilgrim