Zanurkuj w Pythonie/Obsługa błędów
Mimo wielkiego wysiłku wkładanego w pisanie testów jednostkowych błędy wciąż się zdarzają. Co mam na myśli pisząc "błąd"? Błąd to przypadek testowy, który nie został jeszcze napisany.
Przykład 15.1. Błąd
>>> import roman5 >>> roman5.fromRoman("") #(1) 0
- Czy pamiętasz poprzedni rozdział, w którym okazało się, że pusty napis pasuje do wyrażenia regularnego używanego do sprawdzania poprawności liczb rzymskich? Otóż okazuje się, że jest to prawdą nawet w ostatecznej wersji wyrażenia regularnego. I to jest właśnie błąd; pożądanym rezultatem przekazania pustego napisu, podobnie jak każdego innego napisu, który nie reprezentuje poprawnej liczby rzymskiej, jest rzucenie wyjątku InvalidRomanNumeralError.
Po udanym odtworzeniu błędu, ale przed jego naprawieniem, powinno się napisać przypadek testowy, który nie działa, uwidaczniając w ten sposób znaleziony błąd.
Przykład 15.2. Testowanie błędu (romantest61.py)
class FromRomanBadInput(unittest.TestCase):
# previous test cases omitted for clarity (they haven't changed)
def testBlank(self):
"""fromRoman should fail with blank string"""
self.assertRaises(roman.InvalidRomanNumeralError, roman.fromRoman, "") #(1)
- Sprawa jest prosta. Wywołujemy fromRoman z pustym napisem i upewniamy się, że został rzucony wyjątek InvalidRomanNumeralError. Najtrudniejszą częścią było znalezienie błędu; teraz, kiedy już o nim wiemy, testowanie okazuje się łatwe.
Mamy już odpowiedni przypadek testowy, jednak nie będzie on działał, ponieważ w kodzie wciąż jest błąd:
Przykład 15.3. Wyjście programu romantest61.py testującego roman61.py
fromRoman should only accept uppercase input ... ok toRoman should always return uppercase ... ok fromRoman should fail with blank string ... FAIL fromRoman should fail with malformed antecedents ... ok fromRoman should fail with repeated pairs of numerals ... ok fromRoman should fail with too many repeated numerals ... ok fromRoman should give known result with known input ... ok toRoman should give known result with known input ... ok fromRoman(toRoman(n))==n for all n ... ok toRoman should fail with non-integer input ... ok toRoman should fail with negative input ... ok toRoman should fail with large input ... ok toRoman should fail with 0 input ... ok ====================================================================== FAIL: fromRoman should fail with blank string ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage6\romantest61.py", line 137, in testBlank self.assertRaises(roman61.InvalidRomanNumeralError, roman61.fromRoman, "") File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises raise self.failureException, excName AssertionError: InvalidRomanNumeralError ---------------------------------------------------------------------- Ran 13 tests in 2.864s FAILED (failures=1)
Teraz możemy przystąpić do naprawy błędu.
Przykład 15.4. Poprawiane błędu (roman62.py)
Plik jest dostępny w katalogu py/roman/stage6/ znajdującym się w katalogu z przykładami.
def fromRoman(s): """convert Roman numeral to integer""" if not s: #(1) raise InvalidRomanNumeralError, 'Input can not be blank' if not re.search(romanNumeralPattern, s): raise InvalidRomanNumeralError, 'Invalid Roman numeral: %s' % s result = 0 index = 0 for numeral, integer in romanNumeralMap: while s[index:index+len(numeral)] == numeral: result += integer index += len(numeral) return result
- Potrzebne są tylko dwie dodatkowe linie kodu: jawne sprawdzenie pustego napisu oraz wyrażenie raise.
Przykład 15.5. Wyjście programu romantest62.py testującego roman62.py
fromRoman should only accept uppercase input ... ok toRoman should always return uppercase ... ok fromRoman should fail with blank string ... ok #(1) fromRoman should fail with malformed antecedents ... ok fromRoman should fail with repeated pairs of numerals ... ok fromRoman should fail with too many repeated numerals ... ok fromRoman should give known result with known input ... ok toRoman should give known result with known input ... ok fromRoman(toRoman(n))==n for all n ... ok toRoman should fail with non-integer input ... ok toRoman should fail with negative input ... ok toRoman should fail with large input ... ok toRoman should fail with 0 input ... ok ---------------------------------------------------------------------- Ran 13 tests in 2.834s OK #(2)
- Test pustego napisu przechodzi, a więc błąd udało się naprawić.
- Wszystkie pozostałe testy przechodzą, co oznacza, że poprawka błędu nie zepsuła kodu w innych miejscach. Koniec kodowania.
Ten sposób kodowania nie sprawi, że znajdowanie błędów stanie się łatwiejsze. Proste błędy (takie, jak ten w przykładzie) wymagają prostych testów jednostkowych; błędy bardziej złożone będą wymagały testów odpowiednio bardziej złożonych. W środowisku, w którym na testowanie kładzie się duży nacisk, może się początkowo wydawać, że poprawienie błędu zabiera znacznie więcej czasu: najpierw należy dokładnie wyrazić w kodzie, na czym polega błąd (czyli napisać przypadek testowy), a później dopiero go poprawić. Następnie, jeśli przypadek testowy nie przechodzi, należy sprawdzić, czy to poprawka była niewystarczająca, czy może kod przypadku testowego został niepoprawnie zaimplementowany. Jednak w długiej perspektywie takie przełączanie się między kodem i testami niewątpliwie się opłaca, ponieważ poprawienie błędu za pierwszym razem jest o wiele bardziej prawdopodobne. Dodatkowo, możliwość uruchomienia wszystkich testów łącznie z dopisanym nowym przypadkiem testowym pozwala łatwo sprawdzić, czy poprawka błędu nie spowodowała problemów w starym kodzie. Dzisiejszy test jednostkowy staje się więc jutrzejszym testem regresyjnym.