7.6. roman.py, fase 1

Ora che i nostri test delle unità di codice sono pronti, è tempo di cominciare a scrivere il codice che stiamo cercando di verificare con i nostri test. Faremo questo in più fasi, in modo che si possa osservare dapprima come tutti i test falliscano, e poi come a poco a poco abbiano successo man mano che riempiamo gli spazi vuoti all'interno del modulo roman.py.

Esempio 7.7. Il modulo roman1.py

Se non lo avete ancora fatto, potete scaricare questo ed altri esempi usati in questo libro.

"""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 Questo è il modo di definire le proprie eccezioni specifiche in Python. Le eccezioni sono classi, e voi create le vostre specializzando le eccezioni già esistenti. È fortemente raccomandato (ma non richiesto) di specializzare la classe Exception, che è la classe base da cui ereditano tutte le eccezioni predefinite. Qui io sto definendo RomanError (derivato da Exception), che funzionerà da classe base per tutte le mie eccezioni specifiche che saranno definite in seguito. Questa è una questione di stile; avrei potrei altrettanto facilmente derivare ogni singola eccezione direttamente dalla classe Exception.
2 Le eccezioni OutOfRangeError e NotIntegerError saranno in futuro usate da toRoman per segnalare varie forme di input non valido, come specificato in ToRomanBadInput.
3 L'eccezione InvalidRomanNumeralError sarà in futuro usata da fromRoman per segnalare input non valido, come specificato in FromRomanBadInput.
4 In questa fase, noi vogliamo definire le API di ciascuna delle nostre funzioni, ma non vogliamo ancora scrivere il codice, cosi creiamo delle funzioni vuote usando la parola chiave di Python pass.

Siamo arrivati al grande momento (rullo di tamburi, prego): stiamo finalmente per eseguire i nostri test su questo piccolo modulo ancora poco “formato”. A questo punto, tutti i test dovrebbe fallire. In effetti, se un test ha successo nella prima fase, vuol dire che dovremmo ritornare sul modulo romantest.py e riconsiderare il perché abbiamo scritto un test così inutile da avere successo anche con queste funzioni nullafacenti.

Eseguite romantest1.py con l'opzione di linea di comando -v, che vi darà un output più prolisso, cosicché si possa vedere esattamente che cosa succede in ogni test. Con un po di fortuna, il vostro output dovrebbe somigliare a questo:

Esempio 7.8. Output del modulo romantest1.py eseguito su 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 testDecimal
    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 Eseguendo lo script, si lancia unittest.main(), che esegue ciascuno dei test, vale a dire ciascuno dei metodi definiti in ciascuna classe presente in romantest.py. Per ogni test, viene stampata la doc string del metodo comunque, sia se il test ha avuto successo sia se fallisce. Come ci si aspettava, nessuno dei nostri test ha avuto successo.
2 Per ciascun test fallito, unittest visualizza la traccia di esecuzione che mostra esattamente cos'è successo. In questo caso, la nostra chiamata al metodo assertRaises (che ha anche il nome di failUnlessRaises) solleva una eccezione AssertionError, perché si aspettava che toRoman sollevasse un'eccezione OutOfRangeError e questo non è successo.
3 Dopo il dettaglio delle esecuzioni dei test, unittest visualizza un sommario di quanti test sono stati eseguiti e quanto tempo è stato impiegato.
4 In termini generali, il test dell'unità di codice è fallito perché almeno un test non ha avuto sucesso, Quando un test fallisce, unittest distingue tra fallimenti ed errori. Un fallimento è una chiamata ad un metodo del tipo assertXYZ, come assertEqual o assertRaises, che fallisce perché la condizione verificata non è vera o l'eccezione attesa non è stata sollevata. Un errore è ogni altro tipo di eccezione sollevata nel codice che si sta verificando o nello stesso codice di test. Per esempio, il metodo testFromRomanCase (“il metodo fromRoman dovrebbe accettare solo input in lettere maiuscole”) ha dato come risultato un errore, perché la chiamata a numeral.upper() ha sollevato un'eccezione AttributeError, dovuta al fatto che toRoman avrebbe dovuto restituire una stringa, ma non lo ha fatto. Mentre la chiamata al metodo testZero (“toRoman non dovrebbe accettare come dato in ingresso lo 0”) è fallita perché la chiamata al metodo fromRoman non solleva l'eccezione InvalidRomanNumeral alla quale assertRaises è preposta.