7.4. Verificare i casi di errore

Non è abbastanza verificare che la nostra funzione abbia successo quando gli input sono validi; occorre anche verificare che la funzione vada in errore quando riceve input non validi. E non basta che vada in errore: deve farlo nel modo che ci si aspetta.

Ricordiamoci di due altri requisiti per toRoman:

  1. La funzione toRoman dovrebbe andare in errore con un intero fuori dall'intervallo da 1 a 3999.
  2. La funzione toRoman dovrebbe andare in errore con un numero non decimale.

In Python, le funzioni indicano gli errori sollevando eccezioni, ed il modulo unittest fornisce metodi per verificare se una funzione solleva una particolare eccezione quando riceve un input non valido.

Esempio 7.3. Verificare la funzione toRoman con input non validi


class ToRomanBadInput(unittest.TestCase):                            
    def testTooLarge(self):                                          
        """toRoman should fail with large input"""                   
        self.assertRaises(roman.OutOfRangeError, roman.toRoman, 4000) 1

    def testZero(self):                                              
        """toRoman should fail with 0 input"""                       
        self.assertRaises(roman.OutOfRangeError, roman.toRoman, 0)    2

    def testNegative(self):                                          
        """toRoman should fail with negative input"""                
        self.assertRaises(roman.OutOfRangeError, roman.toRoman, -1)  

    def testDecimal(self):                                           
        """toRoman should fail with non-integer input"""             
        self.assertRaises(roman.NotIntegerError, roman.toRoman, 0.5)  3
1 La classe TestCase del modulo unittest fornisce il metodo assertRaises, che accetta i seguenti argomenti: l'eccezione attesa, la funzione sotto test e gli argomenti da passare alla funzione. Se la funzione sotto test richiede più di un argomento, passateli nell'ordine giusto al metodo assertRaises e questi li passerà a sua volta nello stesso ordine alla funzione da verificare. Fate attenzione a quello che si sta facendo a questo punto: invece di chiamare direttamente toRoman e verificare manualmente che sollevi una particolare eccezione (incapsulandola in un blocco try...except), il metodo assertRaises si incarica al posto nostro di chiamare toRoman con il suo argomento (4000) e si assicura che sollevi l'eccezione roman.OutOfRangeError. Ho ricordato di recente com'è comodo che tutto in Python sia un oggetto, incluse funzioni ed eccezioni?
2 Oltre ad effettuare test con numeri troppo grandi, abbiamo bisogno di fare test con numeri troppo piccoli. Ricordiamoci che i numeri romani non possono esprimere lo zero o quantità negative, per cui è opportuno predisporre dei test per questi casi (testZero e testNegative). In testZero, si sta verificando che toRoman sollevi un'eccezione roman.OutOfRangeError quando è chiamata con 0; se non solleva tale eccezione (sia che restituisca un valore o che sollevi un'eccezione diversa), il test è considerato fallito.
3 Il requisito #3 specifica che la funzione toRoman non può accettare un numero decimale, perciò in questo punto si sta verificando che la funzione toRoman sollevi un'eccezione roman.NotIntegerError quando chiamata con un parametro decimale (0.5). Se la funzione toRoman non solleva un eccezione roman.NotIntegerError, questo test è considerato fallito.

I prossimi due requisiti sono simili ai primi tre, fatta eccezione per il fatto che si applicano a fromRoman invece che a toRoman.

  1. La funzione fromRoman dovrebbe accettare un valido numero romano e restituire il numero arabo corrispondente.
  2. La funzione fromRoman dovrebbe andare in errore quando è chiamata con un numero romano non valido.

IL requisito #4 è gestito nello stesso modo del requisito #1, iterando attraverso un campione di valori noti ed effettuando una verifica con ciascuno di essi. Il requisito #5 è gestito alla stessa maniera dei requisiti #2 e #3, verificando la funzione con una serie di input non validi e controllando che sollevi le opportune eccezioni.

Esempio 7.4. Verificare fromRoman con input non validi


class FromRomanBadInput(unittest.TestCase):                                      
    def testTooManyRepeatedNumerals(self):                                       
        """fromRoman should fail with too many repeated numerals"""              
        for s in ('MMMM', 'DD', 'CCCC', 'LL', 'XXXX', 'VV', 'IIII'):             
            self.assertRaises(roman.InvalidRomanNumeralError, roman.fromRoman, s) 1

    def testRepeatedPairs(self):                                                 
        """fromRoman should fail with repeated pairs of numerals"""              
        for s in ('CMCM', 'CDCD', 'XCXC', 'XLXL', 'IXIX', 'IVIV'):               
            self.assertRaises(roman.InvalidRomanNumeralError, roman.fromRoman, s)

    def testMalformedAntecedent(self):                                           
        """fromRoman should fail with malformed antecedents"""                   
        for s in ('IIMXCC', 'VX', 'DCM', 'CMM', 'IXIV',
                  'MCMC', 'XCX', 'IVI', 'LM', 'LD', 'LC'):                       
            self.assertRaises(roman.InvalidRomanNumeralError, roman.fromRoman, s)
1 Non c'è molto da dire su queste linee di codice; lo schema è esattamente lo stesso che è stato usato per verificare toRoman con input non validi. Faccio brevemente notare che si è definita una nuova eccezione: roman.InvalidRomanNumeralError. Questo fa un totale di tre eccezioni specifiche che bisognerà definire in roman.py (contando anche roman.OutOfRangeError e roman.NotIntegerError). Vedremo come definire queste eccezioni specifiche quando cominceremo effettivamente a scrivere roman.py, più avanti in questo capitolo.