Zanurkuj w Pythonie/roman.py, etap 1

Teraz, gdy już są gotowe testy jednostkowe, nadszedł czas na napisanie testowanego przez nie kodu. Zrobimy to w kilku etapach, dzięki czemu będziecie mieli okazję najpierw zobaczyć, że wszystkie testy kończą się niepowodzeniem, a następnie prześledzić, w jaki sposób zaczynają przechodzić, jeden po drugim, tak, że w końcu zapełnione zostaną wszelkie luki w module roman1.py.

Przykład 14.1. roman1.py

Plik jest dostępny w katalogu in py/roman/stage1/ wewnątrz katalogu examples.

Jeśli jeszcze tego nie zrobiliście, możecie pobrać ten oraz inne przykłady używane w tej książce stąd.

"""Convert to and from Roman numerals"""

#Define exceptions
class RomanError(Exception): pass                #(1)
class OutOfRangeError(RomanError): pass          #(2)
class NotIntegerError(RomanError): pass
class InvalidRomanNumeralError(RomanError): pass #(3)

def toRoman(n):
    """convert integer to Roman numeral"""
    pass                                         #(4)

def fromRoman(s):
    """convert Roman numeral to integer"""
    pass
  1. W ten sposób w języku Python definiujemy nasze własne wyjątki. Wyjątki są klasami, a tworzy się je przez utworzenie klasy pochodnej po jednej z już istniejących klas reprezentujących wyjątki. Zaleca się (choć nie jest to wymagane), aby klasy pochodne tworzyć po klasie Exception będącej klasą bazową dla wszystkich wyjątków wbudowanych. W tym miejscu definiuję RomanError, która stanowić będzie kasą bazową dla wszystkich nowych klas wyjątków, o których powiem później. Utworzenie bazowej klasy wyjątku jest kwestią stylu; równie łatwo mógłbym każdą nową klasę wyjątku wyprowadzić bezpośrednio z klasy Exception.
  2. Wyjątki OutOfRangeError oraz NotIntegerError będą wykorzystywane przez funkcję fromRoman do poinformowania otoczenia o różnych nieprawidłowościach w danych wejściowych, tak jak zostało to zdefiniowane w ToRomanBadInput.
  3. Wyjątek InvalidRomanNumeralError będzie wykorzystany przez funkcję fromRoman do oznaczenia nieprawidłowości w danych wejściowych, tak jak zostało to zdefiniowane w FromRomanBadInput.
  4. Na tym etapie dążymy do tego, aby zdefiniować API każdej z naszych funkcji, jednak nie chcemy jeszcze pisać ich kodu. Sygnalizujemy to używając słowa kluczowego pass.

Nadeszła teraz wielka chwila (wchodzą werble!): możemy w końcu uruchomić testy na naszym małym, kadłubkowym module. W tej chwili każdy przypadek testowy powinien zakończyć się niepowodzeniem. W istocie, jeśli na etapie 1 którykolwiek test przejdzie, powinniśmy wrócić do romantests.py i zastanowić się, dlaczego napisaliśmy tak bezużyteczny test, że przechodzi on dla funkcji, które w rzeczywistości nic nie robią.

Uruchomcie romantest1.py podając w linii poleceń opcję -v, dzięki której otrzymamy dokładniejsze informacje i będziemy mogli prześledzić, ktory test jest uruchamiany. Przy odrobinie szczęścia wyjście powinno wyglądać tak:

Przykład 14.2. Wyjście programu romantest1.py testującego roman1.py

fromRoman should only accept uppercase input ... ERROR
toRoman should always return uppercase ... ERROR
fromRoman should fail with malformed antecedents ... FAIL
fromRoman should fail with repeated pairs of numerals ... FAIL
fromRoman should fail with too many repeated numerals ... FAIL
fromRoman should give known result with known input ... FAIL
toRoman should give known result with known input ... FAIL
fromRoman(toRoman(n))==n for all n ... FAIL
toRoman should fail with non-integer input ... FAIL
toRoman should fail with negative input ... FAIL
toRoman should fail with large input ... FAIL
toRoman should fail with 0 input ... FAIL

======================================================================
ERROR: fromRoman should only accept uppercase input
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 154, in testFromRomanCase
    roman1.fromRoman(numeral.upper())
AttributeError: 'None' object has no attribute 'upper'
======================================================================
ERROR: toRoman should always return uppercase
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 148, in testToRomanCase
    self.assertEqual(numeral, numeral.upper())
AttributeError: 'None' object has no attribute 'upper'
======================================================================
FAIL: fromRoman should fail with malformed antecedents
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 133, in testMalformedAntecedent
    self.assertRaises(roman1.InvalidRomanNumeralError, roman1.fromRoman, s)
  File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
    raise self.failureException, excName
AssertionError: InvalidRomanNumeralError
======================================================================
FAIL: fromRoman should fail with repeated pairs of numerals
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 127, in testRepeatedPairs
    self.assertRaises(roman1.InvalidRomanNumeralError, roman1.fromRoman, s)
  File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
    raise self.failureException, excName
AssertionError: InvalidRomanNumeralError
======================================================================
FAIL: fromRoman should fail with too many repeated numerals
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 122, in testTooManyRepeatedNumerals
    self.assertRaises(roman1.InvalidRomanNumeralError, roman1.fromRoman, s)
  File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
    raise self.failureException, excName
AssertionError: InvalidRomanNumeralError
======================================================================
FAIL: fromRoman should give known result with known input
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 99, in testFromRomanKnownValues
    self.assertEqual(integer, result)
  File "c:\python21\lib\unittest.py", line 273, in failUnlessEqual
    raise self.failureException, (msg or '%s != %s' % (first, second))
AssertionError: 1 != None
======================================================================
FAIL: toRoman should give known result with known input
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 93, in testToRomanKnownValues
    self.assertEqual(numeral, result)
  File "c:\python21\lib\unittest.py", line 273, in failUnlessEqual
    raise self.failureException, (msg or '%s != %s' % (first, second))
AssertionError: I != None
======================================================================
FAIL: fromRoman(toRoman(n))==n for all n
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 141, in testSanity
    self.assertEqual(integer, result)
  File "c:\python21\lib\unittest.py", line 273, in failUnlessEqual
    raise self.failureException, (msg or '%s != %s' % (first, second))
AssertionError: 1 != None
======================================================================
FAIL: toRoman should fail with non-integer input
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 116, in testNonInteger
    self.assertRaises(roman1.NotIntegerError, roman1.toRoman, 0.5)
  File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
    raise self.failureException, excName
AssertionError: NotIntegerError
======================================================================
FAIL: toRoman should fail with negative input
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 112, in testNegative
    self.assertRaises(roman1.OutOfRangeError, roman1.toRoman, -1)
  File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
    raise self.failureException, excName
AssertionError: OutOfRangeError
======================================================================
FAIL: toRoman should fail with large input
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 104, in testTooLarge
    self.assertRaises(roman1.OutOfRangeError, roman1.toRoman, 4000)
  File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
    raise self.failureException, excName
AssertionError: OutOfRangeError
======================================================================
FAIL: toRoman should fail with 0 input                                 #(1)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 108, in testZero
    self.assertRaises(roman1.OutOfRangeError, roman1.toRoman, 0)
  File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
    raise self.failureException, excName
AssertionError: OutOfRangeError                                        #(2)
----------------------------------------------------------------------
Ran 12 tests in 0.040s                                                 #(3)

FAILED (failures=10, errors=2)                                         #(4)
  1. Po uruchomieniu skryptu zostaje wywołana funkcja unittest.main(), która z kolei wywołuje każdą z metod zdefiniowanych w każdej klasie wewnątrz romantest.py. Dla każdego przypadku testowego wypisywany jest napis dokumentujący odpowiadającej mu metody oraz to, czy przypadek testowy przeszedł, czy nie. Tak, jak się spodziewaliśmy, żaden test nie przeszedł.
  2. Dla każdego przypadku testowego, który zakończył się niepowodzeniem, unittest wypisuje zawartość stosu, dzięki czemu widać dokładnie, co się stało. W tym przypadku wywołanie funkcji assertRaises (znanej również pod nazwą failUnlessRaises) spowodowało rzucenie wyjątku AssertionError z tego powodu, że w teście spodziewaliśmy się, że toRoman rzuci OutOfRangeError, a taki wyjątek nie został rzucony.
  3. Po wypisaniu szczegółów, unittest wypisuje podsumowanie zawierające informacje o tym, ile testów zostało uruchomionych oraz jak długo one trwały.
  4. Ogólnie rzecz biorąc, test jednostkowy nie przechodzi, jeśli przynajmniej jeden przypadek testowy nie przechodzi. Kiedy przypadek testowy nie przejdzie, unittest rozróżnia niepowodzenia (failures) i błędy (errors). Niepowodzenie występuje w przypadku wywołań metod assertXYZ, np. assertEqual czy assertRaises, które kończą się niepowodzeniem, ponieważ nie został spełniony pewien zakładany warunek albo nie został rzucony spodziewany wyjątek. Błąd natomiast występuje wówczas, gdy zostanie rzucony jakikolwiek inny wyjątek i to zarówno w kodzie testowanym, jak i w kodzie samego testu. Na przykład błąd wystąpił w metodzie testFromRomanCase ("Funkcja fromRoman powinna akceptować na wejściu napisy zawierające wyłącznie wielkie litery"), ponieważ wywołanie numeral.upper() rzuciło wyjątek AttributeError: toRoman miało zwrócić napis, a tego nie zrobiło. Natomiast testZero ("Funkcja toRoman otrzymująca na wejściu wartość 0 powinna zakończyć się niepowodzeniem") zakończyła się niepowodzeniem, ponieważ wywołanie fromRoman nie rzuciło wyjątku InvalidRomanNumeral, którego spodziewał się assertRaises.