Itt vagy: Kezdőlap Ugorj fejest a Python 3-ba

Nehézségi szint: ♦♦♦♦♢

Átdolgozás

Miután valaki rengeteg és még annál is több hangot játszott le, a művészet megkoronázásaként az egyszerűség emelkedik ki.
Frédéric Chopin

 

Ugorj fejest

Akár tetszik, akár nem, előfordulnak programhibák. Az átfogó egységtesztek írására tett legnagyobb erőfeszítéseid ellenére, előfordulnak programhibák. Mit értek „programhiba” alatt? A programhiba egy olyan teszteset, amit még nem írtál meg.

>>> import roman7
>>> roman7.from_roman('') 
0
  1. Ez egy programhiba. Az üres karakterlánc hatására InvalidRomanNumeralError kivételt kellene dobnia, ahogyan minden, nem érvényes római számot képviselő karakterlánc esetén is.

A programhiba reprodukálása után, és javítása előtt írnod kell egy tesztesetet, amely sikertelenségével bemutatja a programhibát.

class FromRomanBadInput(unittest.TestCase):  
    .
    .
    .
    def testBlank(self):
        '''a from_roman nem engedélyezhet üres karakterláncot'''
        self.assertRaises(roman6.InvalidRomanNumeralError, roman6.from_roman, '') 
  1. Ez elég egyszerű. Hívjuk meg a from_roman() függvényt egy üres karakterlánccal, és győződjünk meg róla, hogy InvalidRomanNumeralError kivételt dob. A nehéz feladat a programhiba megtalálása volt: most hogy tudsz róla, a tesztelés már könnyű.

Mivel a kódban jelen van a programhiba, és immár van egy ezt tesztelő teszteseted, a tesztelés sikertelen lesz:

you@localhost:~/diveintopython3/examples$ python3 romantest8.py -v
a from_roman nem engedélyezhet üres karakterláncot ... FAIL
a from_roman nem engedélyezhet rosszul formázott előtagokat ... ok
a from_roman nem engedélyezhet ismétlődő számpárokat ... ok
a from_roman nem engedélyezhet túl sok ismétlődő karaktert ... ok
a from_roman ismert eredményt kell adjon ismert bemenetre ... ok
a to_roman ismert eredményt kell adjon ismert bemenetre ... ok
from_roman(to_roman(n))==n minden n-re ... ok
a to_roman nem engedélyezhet negatív bemenetet ... ok
a to_roman nem engedélyezhet nem egész bemenetet ... ok
a to_roman nem engedélyezhet túl nagy bemenetet ... ok
a to_roman nem engedélyezheti a 0 bemenetet ... ok

======================================================================
FAIL: a from_roman nem engedélyezhet üres karakterláncot
----------------------------------------------------------------------
Traceback (most recent call last):
  File "romantest8.py", line 117, in test_blank
    self.assertRaises(roman8.InvalidRomanNumeralError, roman8.from_roman, '')
AssertionError: InvalidRomanNumeralError not raised by from_roman

----------------------------------------------------------------------
Ran 11 tests in 0.171s

FAILED (failures=1)

Most kijavíthatod a programhibát.

def from_roman(s):
    '''római számok egésszé alakítása'''
    if not s:                                                                  
        raise InvalidRomanNumeralError('A bemenet nem lehet üres')
    if not re.search(romanNumeralPattern, s):
        raise InvalidRomanNumeralError('Érvénytelen római szám: {}'.format(s))  

    result = 0
    index = 0
    for numeral, integer in romanNumeralMap:
        while s[index:index+len(numeral)] == numeral:
            result += integer
            index += len(numeral)
    return result
  1. Csak két sor kód szükséges: az egyik ellenőrzi, hogy üres-e a karakterlánc, a másik pedig egy raise utasítás.
  2. Nem hiszem, hogy már említettem ezt valahol a könyvben, de akkor most álljon itt utolsó leckeként a karakterláncok formázásáról. A Python 3.1 óta kihagyhatod a számokat a formátum-előírásokban lévő pozicionális indexekből. Azaz a {0} formátum-előírás helyett a format() metódus első paraméterére egyszerűen a {} használatával is hivatkozhatsz, és a Python kitölti helyetted a megfelelő pozicionális indexet. Ez tetszőleges számú argumentum esetén is működik: az első {} jelentése {0}, a második {} jelentése {1}, és így tovább.
you@localhost:~/diveintopython3/examples$ python3 romantest8.py -v
a from_roman nem engedélyezhet üres karakterláncot ... ok  
a from_roman nem engedélyezhet rosszul formázott előtagokat ... ok
a from_roman nem engedélyezhet ismétlődő számpárokat ... ok
a from_roman nem engedélyezhet túl sok ismétlődő karaktert ... ok
a from_roman ismert eredményt kell adjon ismert bemenetre ... ok
a to_roman ismert eredményt kell adjon ismert bemenetre ... ok
from_roman(to_roman(n))==n minden n-re ... ok
a to_roman nem engedélyezhet negatív bemenetet ... ok
a to_roman nem engedélyezhet nem egész bemenetet ... ok
a to_roman nem engedélyezhet túl nagy bemenetet ... ok
a to_roman nem engedélyezheti a 0 bemenetet ... ok

----------------------------------------------------------------------
Ran 11 tests in 0.156s

OK  
  1. Az üres karakterlánc teszteset most már sikeres, így a programhiba javítva van.
  2. Az összes többi teszteset továbbra is átmegy, vagyis ennek a programhibának a javítása nem rontott el semmi mást. Fejezd be a kódolást.

Ez a kódolási stílus nem teszi könnyebbé a programhibák javítását. Az egyszerű programhibák (mint ez) egyszerű teszteseteket, a bonyolult programhibák bonyolult teszteseteket igényelnek. Egy tesztelésközpontú környezetben úgy tűnhet, mintha tovább tartana egy programhiba javítása, mivel kódban kell kifejezned a programhibát (a teszteset megírásához), és csak ezután javíthatod ki magát a programhibát. Ezután ha a teszteset nem lesz azonnal sikeres, meg kell találnod, hogy a programhiba javítása volt-e rossz, vagy maga a teszteset lett-e hibás. Azonban hosszú távon ez az oda-vissza ugrálás a kód tesztelése és a tesztelt kód között kifizetődik, mert valószínűbbé teszi, hogy a programhibák már első alkalommal is helyesen kerültek kijavításra. Továbbá, mivel az újjal együtt egyszerűen újrafuttathatod az összes tesztesetet, sokkal kevésbé valószínű, hogy az új kód javítása során véletlenül elrontasz valami régi kódot. A ma egységtesztje a holnap regressziótesztje.

Változó követelmények kezelése

Legnagyobb erőfeszítéseid dacára, amelyek az ügyfeleid földhöz szögezésére és belőlük a pontos követelmények kiszedésére vonatkoznak, például ollók és forró viasz által okozott fájdalom útján, a követelmények változni fognak. A legtöbb ügyfél nem tudja mit akar, amíg nem látja azt; és még ha tudják is, nem különösebben jók a kívánságaik használható részletességgel való megfogalmazásában. Ha pedig mégis, akkor a következő kiadásban többet fognak akarni. Emiatt készülj fel a teszteseteid frissítésére a követelmények változásával együtt.

Tegyük fel például, hogy ki szeretnéd terjeszteni a római számokat átalakító függvények tartományát. Általában a római számokban egyik karakter sem ismétlődhet egymás után háromnál többször. De a rómaiak szívesen tettek kivételt ez alól a szabály alól, és megengedtek egymás után 4 M karaktert a 4000 ábrázolásához. Ha elvégzed ezt a módosítást, akkor kiterjesztheted az átalakítható számok tartományát1..3999-ről 1..4999-re. De előbb a teszteseteidet kell módosítanod.

[a roman8.py letöltése]

class KnownValues(unittest.TestCase):
    known_values = ( (1, 'I'),
                      .
                      .
                      .
                     (3999, 'MMMCMXCIX'),
                     (4000, 'MMMM'),                                      
                     (4500, 'MMMMD'),
                     (4888, 'MMMMDCCCLXXXVIII'),
                     (4999, 'MMMMCMXCIX') )

class ToRomanBadInput(unittest.TestCase):
    def test_too_large(self):
        '''a to_roman nem engedélyezhet túl nagy bemenetet'''
        self.assertRaises(roman8.OutOfRangeError, roman8.to_roman, 5000)  

    .
    .
    .

class FromRomanBadInput(unittest.TestCase):
    def test_too_many_repeated_numerals(self):
        '''a from_roman nem engedélyezhet túl sok ismétlődő karaktert'''
        for s in ('MMMMM', 'DD', 'CCCC', 'LL', 'XXXX', 'VV', 'IIII'):     
            self.assertRaises(roman8.InvalidRomanNumeralError, roman8.from_roman, s)

    .
    .
    .

class RoundtripCheck(unittest.TestCase):
    def test_roundtrip(self):
        '''from_roman(to_roman(n))==n minden n-re'''
        for integer in range(1, 5000):                                    
            numeral = roman8.to_roman(integer)
            result = roman8.from_roman(numeral)
            self.assertEqual(integer, result)
  1. A meglévő ismert értékek nem változnak (még mindig van értelme ezeket mind tesztelni), de hozzá kell adnod még néhányat a 4000-es tartományban. Itt felvettem a 4000 (a legrövidebb), a 4500 (a második legrövidebb), a 4888 (a leghosszabb) és a 4999 (a legnagyobb) értékeket.
  2. A „nagy bemenet” meghatározása megváltozott. Ez a teszt a to_roman() függvényt a 4000 értékkel hívta, és hibát várt. Most hogy a 4000-4999 tartomány is jónak számít, ezt meg kell növelned 5000-re.
  3. A „túl sok ismétlődő szám” definíciója is megváltozott. Ez a teszt a from_roman() függvényt az 'MMMM' értékkel hívta, és hibát várt. Most hogy az MMMM is érvényes római számnak számít, ezt meg kell növelned 'MMMMM'-re.
  4. Az épségellenőrzés végigmegy minden számon a tartományban 1 és 3999 között. Mivel a tartomány kibővült, ezt a for ciklust is frissítened kell a 4999-ig haladáshoz.

Most már minden teszteseted az új követelményekhez van igazítva, de a kódod még nem, így valószínűleg több teszteset is sikertelen lesz.

you@localhost:~/diveintopython3/examples$ python3 romantest9.py -v
a from_roman nem engedélyezhet üres karakterláncot ... ok
a from_roman nem engedélyezhet rosszul formázott előtagokat ... ok
a from_roman nem engedélyezhet nem karakterlánc bemenetet ... ok
a from_roman nem engedélyezhet ismétlődő számpárokat ... ok
a from_roman nem engedélyezhet túl sok ismétlődő karaktert ... ok
a from_roman ismert eredményt kell adjon ismert bemenetre ... ERROR          
a to_roman ismert eredményt kell adjon ismert bemenetre ... ERROR            
from_roman(to_roman(n))==n minden n-re ... ERROR                             
a to_roman nem engedélyezhet negatív bemenetet ... ok
a to_roman nem engedélyezhet nem egész bemenetet ... ok
a to_roman nem engedélyezhet túl nagy bemenetet ... ok
a to_roman nem engedélyezheti a 0 bemenetet ... ok

======================================================================
ERROR: a from_roman ismert eredményt kell adjon ismert bemenetre
----------------------------------------------------------------------
Traceback (most recent call last):
  File "romantest9.py", line 82, in test_from_roman_known_values
    result = roman9.from_roman(numeral)
  File "C:\home\diveintopython3\examples\roman9.py", line 60, in from_roman
    raise InvalidRomanNumeralError('Érvénytelen római szám: {0}'.format(s))
roman9.InvalidRomanNumeralError: Érvénytelen római szám: MMMM

======================================================================
ERROR: a to_roman ismert eredményt kell adjon ismert bemenetre
----------------------------------------------------------------------
Traceback (most recent call last):
  File "romantest9.py", line 76, in test_to_roman_known_values
    result = roman9.to_roman(integer)
  File "C:\home\diveintopython3\examples\roman9.py", line 42, in to_roman
    raise OutOfRangeError('a szám kívül esik a tartományon (0 és 3999 közt kell legyen)')
roman9.OutOfRangeError: a szám kívül esik a tartományon (0 és 3999 közt kell legyen)

======================================================================
ERROR: from_roman(to_roman(n))==n minden n-re
----------------------------------------------------------------------
Traceback (most recent call last):
  File "romantest9.py", line 131, in testSanity
    numeral = roman9.to_roman(integer)
  File "C:\home\diveintopython3\examples\roman9.py", line 42, in to_roman
    raise OutOfRangeError('a szám kívül esik a tartományon (0 és 3999 közt kell legyen)')
roman9.OutOfRangeError: a szám kívül esik a tartományon (0 és 3999 közt kell legyen)

----------------------------------------------------------------------
Ran 12 tests in 0.171s

FAILED (errors=3)
  1. A from_roman() ismert értékek tesztje az 'MMMM' elérésekor azonnal sikertelenné válik, mert a from_roman() ezt még mindig érvénytelen római számnak gondolja.
  2. A to_roman() ismert értékek tesztje a 4000 elérésekor azonnal sikertelenné válik, mert a to_roman() ezt még mindig tartományon kívülinek gondolja.
  3. A körbejárási teszt a 4000 elérésekor szintén sikertelenné válik, mert a to_roman() ezt még mindig tartományon kívülinek gondolja.

Most, hogy vannak az új követelmények miatt sikertelen teszteseteid, kezdhetsz a kód javítására gondolni. (Amikor először elkezdesz egységteszteket írni, furcsának érezheted, hogy a tesztelt kód soha nem jár a tesztesetek „előtt”. Amíg mögöttük van, van tennivalód, és amint utoléri a teszteseteket, befejezed a kódolást. Miután hozzászoktál, azon fogsz csodálkozni, hogy korábban hogyan tudtál egyáltalán tesztek nélkül programozni.)

[a roman9.py letöltése]

roman_numeral_pattern = re.compile('''
    ^                   # karakterlánc eleje
    M{0,4}              # ezresek - 0 és 4 közti M   
    (CM|CD|D?C{0,3})    # százasok - 900 (CM), 400 (CD), 0-300 (0 és 3 közti C),
                        #            vagy 500-800 (D, amelyet 0 és 3 közti C követ)
    (XC|XL|L?X{0,3})    # tizesek - 90 (XC), 40 (XL), 0-30 (0 és 3 közti X),
                        #        vagy 50-80 (L, amelyet 0 és 3 közti X követ)
    (IX|IV|V?I{0,3})    # egyesek - 9 (IX), 4 (IV), 0-3 (0 és 3 közti I),
                        #        vagy 5-8 (V, amelyet 0 és 3 közti I követ)
    $                   # karakterlánc vége
    '''def to_roman(n):
    '''egész szám római számmá alakítása'''
    if not isinstance(n, int):
        raise NotIntegerError('a nem egész számok nem alakíthatók át')
    if not (0 < n < 5000):                        
        raise OutOfRangeError('a szám kívül esik a tartományon (1 és 4999 közt kell legyen)')

    result = ''
    for numeral, integer in roman_numeral_map:
        while n >= integer:
            result += numeral
            n -= integer
    return result

def from_roman(s):
    .
    .
    .
  1. A from_roman() függvényt egyáltalán nem kell módosítanod. Az egyetlen változás a roman_numeral_pattern kifejezést érinti. Ha közelebbről nézed, akkor észre fogod venni, hogy a nem kötelező M karakterek maximális számát megváltoztattam 3-ról 4-re a reguláris kifejezés első részében. Ez 4999-ig teszi lehetővé a római számok használatát a 3999 helyett. A tényleges from_roman() függvény teljesen általános, csupán ismétlődő rómaiszám-karaktereket keres, és összeadja azokat, nem törődve az ismétlések számával. Egyetlen oka volt annak, hogy nem kezelte az 'MMMM'-et korábban: az, hogy kifejezetten megállítottad a reguláris kifejezés mintaillesztésénél.
  2. A to_roman() függvény csak egy kis változtatást igényel a tartomány-ellenőrzésnél. A 0 < n < 4000 ellenőrzésénél mostantól a 0 < n < 5000 áll. Ezen kívül meg kell változtatnod a kivétel dobásánál a raise utasítás hibaüzenetét, hogy az új dob tartományt tükrözze (1..4999 az 1..3999 helyett). A függvény többi részét nem kell módosítanod, az új eseteket már kezeli. (Boldogan hozzáadja az'M'-et az összes megtalált ezreshez, a 4000 megadásakor pedig 'MMMM'-et ad vissza. Az egyetlen ok, amiért ezt korábban nem tette, az az, hogy kifejezetten megállítottad a tartomány-ellenőrzéssel.)

Kételkedhetsz benne, hogy ez a két kis változtatás minden, amire szükséged van. Ne hidd el nekem, próbáld ki magad.

you@localhost:~/diveintopython3/examples$ python3 romantest9.py -v
a from_roman nem engedélyezhet üres karakterláncot ... ok
a from_roman nem engedélyezhet rosszul formázott előtagokat ... ok
a from_roman nem engedélyezhet nem karakterlánc bemenetet ... ok
a from_roman nem engedélyezhet ismétlődő számpárokat ... ok
a from_roman nem engedélyezhet túl sok ismétlődő karaktert ... ok
a from_roman ismert eredményt kell adjon ismert bemenetre ... ok
a to_roman ismert eredményt kell adjon ismert bemenetre ... ok
from_roman(to_roman(n))==n minden n-re ... ok
a to_roman nem engedélyezhet negatív bemenetet ... ok
a to_roman nem engedélyezhet nem egész bemenetet ... ok
a to_roman nem engedélyezhet túl nagy bemenetet ... ok
a to_roman nem engedélyezheti a 0 bemenetet ... ok

----------------------------------------------------------------------
Ran 12 tests in 0.203s

OK  
  1. Minden teszteset átmegy. Fejezd be a kódolást.

Az átfogó egységtesztelés azt jelenti, hogy soha nem kell egy olyan programozóra hagyatkoznod, aki azt mondja: „Bízz bennem.”

Átdolgozás

Az átfogó egységtesztelés legjobb része nem az az érzés, amikor végre minden teszteseted átmegy, vagy akár az az érzés, amikor valaki téged okol a kódja elromlásáért, és be tudod bizonyítani, hogy nem te voltál. Az egységtesztelés legjobb része az, hogy szabadságot ad a kíméletlen átdolgozásra.

Az átdolgozás a működő kód jobban működővé tételét jelenti. Általában a „jobban” azt jelenti, hogy „gyorsabban”, noha jelentheti azt is, hogy „kevesebb memóriát használva”, vagy „kevesebb lemezterületet használva”, vagy egyszerűen csak „elegánsabban”. Akármit is jelent számodra, a projekted számára, a környezetedben, az átdolgozás bármely program hosszú távú egészsége szempontjából fontos.

Itt most a „jobban” egyszerre jelenti azt, hogy „gyorsabban” és hogy „egyszerűbben karbantartható”. Konkrétan a from_roman() függvény lassabb és bonyolultabb, mint ahogy szeretném, mégpedig a római számok ellenőrzéséhez használt nagy, ronda reguláris kifejezés miatt. Most az juthat eszedbe: „Persze, a reguláris kifejezés nagy és ronda, de hogy máshogy ellenőrizhetném, hogy egy tetszőleges karakterlánc érvényes római szám-e?”

Válasz: csak 5000 van belőlük, miért nem készítesz egy kikeresési táblát? Ez az ötlet egyre jobbá válik, amikor rájössz, hogy egyáltalán nem kell reguláris kifejezéseket használnod. Az egészek római számokká alakításához használandó kikeresési tábla felépítése során felépítheted a fordított kikeresési táblát is a római számok egészekké alakításához. Mire ellenőrizned kell, hogy egy tetszőleges karakterlánc érvényes római szám-e, már összegyűjtötted az összes érvényes római számot. Az „ellenőrzés” egyetlen szótári kikereséssé egyszerűsödött.

A legjobb pedig, hogy már van egy teljes sorozat egységteszted. Megváltoztathatod a modul kódjának több, mint felét, de az egységtesztek ugyanazok maradnak. Ez azt jelenti, hogy bebizonyíthatod – magadnak és másoknak –  is, hogy az új kód ugyanolyan jól működik, mint az eredeti.

[a roman10.py letöltése]

class OutOfRangeError(ValueError): pass
class NotIntegerError(ValueError): pass
class InvalidRomanNumeralError(ValueError): pass

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))

to_roman_table = [ None ]
from_roman_table = {}

def to_roman(n):
    '''egész szám római számmá alakítása'''
    if not (0 < n < 5000):
        raise OutOfRangeError('a szám kívül esik a tartományon (1 és 4999 közt kell legyen)')
    if int(n) != n:
        raise NotIntegerError('a nem egész számok nem alakíthatók át')
    return to_roman_table[n]

def from_roman(s):
    '''római számok egésszé alakítása'''
    if not isinstance(s, str):
        raise InvalidRomanNumeralError('A bemenetnek karakterláncnak kell lennie')
    if not s:
        raise InvalidRomanNumeralError('A bemenet nem lehet üres')
    if s not in from_roman_table:
        raise InvalidRomanNumeralError('Érvénytelen római szám: {0}'.format(s))
    return from_roman_table[s]

def build_lookup_tables():
    def to_roman(n):
        result = ''
        for numeral, integer in roman_numeral_map:
            if n >= integer:
                result = numeral
                n -= integer
                break
        if n > 0:
            result += to_roman_table[n]
        return result

    for integer in range(1, 5000):
        roman_numeral = to_roman(integer)
        to_roman_table.append(roman_numeral)
        from_roman_table[roman_numeral] = integer

build_lookup_tables()

Szedjük ezt szét emészthető darabokra. Vitathatalanul a legfontosabb sor az utolsó:

build_lookup_tables()

Észreveheted, hogy ez egy függvényhívás, de nincs körülötte if utasítás. Ez nem egy if __name__ == '__main__' blokk: a modul importálásakor kerül meghívásra. (Fontos megértened, hogy a modulok csak egyszer kerülnek importálásra, azután gyorsítótárazva lesznek. Ha egy már importált modult importálsz, akkor nem történik semmi. Így ez a kód csak a modul első importálásakor kerül meghívásra.)

Tehát mit csinál a build_lookup_tables() függvény? Örülök, hogy megkérdezted!

to_roman_table = [ None ]
from_roman_table = {}
.
.
.
def build_lookup_tables():
    def to_roman(n):                                
        result = ''
        for numeral, integer in roman_numeral_map:
            if n >= integer:
                result = numeral
                n -= integer
                break
        if n > 0:
            result += to_roman_table[n]
        return result

    for integer in range(1, 5000):
        roman_numeral = to_roman(integer)          
        to_roman_table.append(roman_numeral)       
        from_roman_table[roman_numeral] = integer
  1. Ez egy okosan megírt kódrészlet… talán túl okosan is. A to_roman() függvény fentebb van definiálva: kikeresi az értékeket a táblában, és visszaadja azokat. De a build_lookup_tables() függvény átdefiniálja a to_roman() függvényt, hogy valójában csináljon is valamit (mint a korábbi példák, mielőtt elkezdtél volna kikeresési táblát használni). A build_lookup_tables() függvényen belül a to_roman() hívása ezt az átdefiniált verziót fogja meghívni. Amikor a build_lookup_tables() függvény kilép, az átdefiniált verzió eltűnik – az csak a build_lookup_tables() függvény helyi hatókörében van definiálva.
  2. Ez a kódsor meghívja az átdefiniált to_roman() függvényt, amely valójában kiszámítja a római számot.
  3. Miután megvan az eredmény (az átdefiniált to_roman() függvényből), felveszed az egészet és a római számos megfelelőjét mindkét kikeresési táblába.

Miután a kikeresési táblák felépültek, a kód többi része egyszerű és gyors is.

def to_roman(n):
    '''egész szám római számmá alakítása'''
    if not (0 < n < 5000):
        raise OutOfRangeError('a szám kívül esik a tartományon (1 és 4999 közt kell legyen)')
    if int(n) != n:
        raise NotIntegerError('a nem egész számok nem alakíthatók át')
    return to_roman_table[n]                                            

def from_roman(s):
    '''római számok egésszé alakítása'''
    if not isinstance(s, str):
        raise InvalidRomanNumeralError('A bemenetnek karakterláncnak kell lennie')
    if not s:
        raise InvalidRomanNumeralError('A bemenet nem lehet üres')
    if s not in from_roman_table:
        raise InvalidRomanNumeralError('Érvénytelen római szám: {0}'.format(s))
    return from_roman_table[s]                                          
  1. A korábbival egyező határellenőrzek után a to_roman() függvény egyszerűen megkeresi a megfelelő értéket a kikeresési táblában, és visszaadja azt.
  2. Hasonlóképpen a from_roman() függvény is némi határellenőrzésre és egy sor kódra zsugorodott. Nincs több reguláris kifejezés. Nincs több ciklus. O(1) átalakítás római számokká és vissza.

De működik ez? Hát hogyne, persze hogy. Be tudom bizonyítani.

you@localhost:~/diveintopython3/examples$ python3 romantest10.py -v
a from_roman nem engedélyezhet üres karakterláncot ... ok
a from_roman nem engedélyezhet rosszul formázott előtagokat ... ok
a from_roman nem engedélyezhet nem karakterlánc bemenetet ... ok
a from_roman nem engedélyezhet ismétlődő számpárokat ... ok
a from_roman nem engedélyezhet túl sok ismétlődő karaktert ... ok
a from_roman ismert eredményt kell adjon ismert bemenetre ... ok
a to_roman ismert eredményt kell adjon ismert bemenetre ... ok
from_roman(to_roman(n))==n minden n-re ... ok
a to_roman nem engedélyezhet negatív bemenetet ... ok
a to_roman nem engedélyezhet nem egész bemenetet ... ok
a to_roman nem engedélyezhet túl nagy bemenetet ... ok
a to_roman nem engedélyezheti a 0 bemenetet ... ok

----------------------------------------------------------------------
Ran 12 tests in 0.031s                                                  

OK
  1. Nem mintha kérdezted volna, de gyors is! Nagyjából 10×. Természetesen az összehasonlítás nem teljesen igazságos, mert ennek a verziónak tovább tart az importálása, (amikor felépíti a kikeresési táblákat). Mivel azonban az importálás csak egyszer történik meg, az indítási költség szétterül a to_roman() és from_roman() függvények összes hívása között. Mivel a tesztek több ezer függvényhívást végeznek (a körbejárás teszt magában 10000-et), ezek a megtakarítások gyorsan összeadódnak!

A történet tanulsága?

Összegzés

Az egységtesztelés egy hatékony alapelv, amelyet megfelelően megvalósítva egyszerre csökkentheti a karbantartási költségeket, és növelheti a rugalmasságot bármely hosszú távú projektben. Fontos megérteni azt is, hogy az egységtesztelés nem csodaszer, Bűvös Problémamegoldó, vagy mindent vivő Lola T. Jó tesztesetek írása nehéz, és a naprakészen tartásuk eltökéltséget igényel (különösen amikor az ügyfelek kritikus hibajavításokat akarnak tegnapra). Az egységtesztelés nem helyettesíti a tesztelés más formáit, beleértve a funkcionális tesztelést, integrációs tesztelést és a felhasználó általi elfogadás tesztelését. Azonban megvalósítható és működik, ha pedig egyszer már láttad működni, azt fogod kérdezni, hogyan tudtál eddig enélkül élni.

Ez a néhány fejezet rengeteg témát felölelt, és ebből sok nem is volt Python-specifikus. Sok nyelvhez léteznek egységtesztelő keretrendszerek, amelyek mindegyike ugyanazon alapvető fogalmak megértését igényli:

© 2001–11 Mark Pilgrim