7.5. Verificare la consistenza

Spesso vi capiterà di scoprire che un'unità di codice contiene un insieme di funzioni reciproche, di solito in forma di funzioni di conversione, laddove una converte A in B e l'altra converte B in A. In questi casi, è utile creare un “test di consistenza” per essere sicuri che si possa converire A in B e poi riconvertire B in A senza perdere precisione, incorrere in errori di arrotondamento o in qualche altro malfunzionamento.

Si consideri questo requisito:

  1. Prendendo un numero arabo, convertendolo in numero romano e poi riconvertendolo in numero arabo, ci si dovrebbe ritrovare con il numero da cui si era partiti. Quindi fromRoman(toRoman(n)) == n per tutti i numeri da 1 a 3999.

Esempio 7.5. Verificare toRoman in confronto con fromRoman


class SanityCheck(unittest.TestCase):        
    def testSanity(self):                    
        """fromRoman(toRoman(n))==n for all n"""
        for integer in range(1, 4000):        1 2
            numeral = roman.toRoman(integer) 
            result = roman.fromRoman(numeral)
            self.assertEqual(integer, result) 3
1 Abbiamo già incontrato la funzione range in precedenza, ma qui è usata con due argomenti, per cui otteniamo una lista di interi a partire dal primo argomento (1) e poi contando successivamente fino al secondo argomento (4000), escludendo quest'ultimo. Cioe l'intervallo da 1 a 3999, che è l'insieme dei numeri arabi convertibili in numeri romani.
2 Vorrei semplicemente citare di passaggio il fatto che integer non è una parola chiave di Python. Qui è giusto un nome di variabile come gli altri.
3 La struttura logica del test è lineare: si prende un numero arabo (integer), lo si converte in numero Romano (numeral), poi lo si riconverte in numero arabo (result) e si controlla che sia uguale al numero di partenza. In caso negativo, assertEqual solleverà un'eccezione ed il test sarà immediatamente considerato fallito. Se tutti i numeri corrispondono, assertEqual terminerà sempre in modo normale, l'intero metodo testSanity terminerà quindi in modo normale ed il test sarà considerato passato con successo.

Gli ultimi due requisiti sono diversi dagli altri perché entrambi danno l'apparenza di essere tanto arbitrari quanto banali:

  1. La funzione toRoman deve sempre restituire un numero romano composto di lettere maiuscole.
  2. La funzione fromRoman dovrebbe accettare in input solo numeri romani composti di lettere maiuscole (i.e. dovrebbe andare in errore con un input in lettere minuscole).

In effetti, questi due requisiti sono un tantino arbitrari. Avremmo per esempio potuto decidere che fromRoman poteva accettare input composti di lettere minuscole oppure sia maiuscole che minuscole. Tuttavia questi requisiti non sono completamente arbitrari; se toRoman restituisce sempre numeri in lettere maiuscole,allora fromRoman deve come minimo accettare input in lettere maiuscole, oppure il nostro “controllo di consistenza” (requisito #6) fallirebbe. Il fatto che fromRoman accetti solo lettere maiuscole è arbitrario, ma come ogni sistemista potrebbe confermare, usare lettere maiuscole o minuscole fa differenza, per cui vale la pena specificarlo sin dall'inizio. E se vale la pena specificarlo, allora vale la pena verificarlo.

Esempio 7.6. Verificare rispetto a maiuscolo/minuscolo


class CaseCheck(unittest.TestCase):                   
    def testToRomanCase(self):                        
        """toRoman should always return uppercase"""  
        for integer in range(1, 4000):                
            numeral = roman.toRoman(integer)          
            self.assertEqual(numeral, numeral.upper()) 1

    def testFromRomanCase(self):                      
        """fromRoman should only accept uppercase input"""
        for integer in range(1, 4000):                
            numeral = roman.toRoman(integer)          
            roman.fromRoman(numeral.upper())           2 3
            self.assertRaises(roman.InvalidRomanNumeralError,
                              roman.fromRoman, numeral.lower())
1 La cosa più interessante di questo test è tutto quello che non verifica. Non verifica che il valore restituito da toRoman sia giusto o perlomeno consistente; a queste domande rispondono altri test. Questo intero test esiste solo per verificare l'uso delle maiuscole. Si potrebbe essere tentati di combinare questo test con quello di consistenza, visto che entrambi ciclano attraverso l'intero intervallo di valori ed entrambi chiamano toRoman. [13] Ma questo violerebbe una delle nostre regole fondamentali: ogni test dovrebbe rispondere ad una sola domanda. Immaginate di combinare questo test con quello di consistenza e che il test fallisca. Dovreste fare ulteriori analisi per scoprire quale parte del test è fallita e così determinare qual'è il problema. Se vi tocca analizzare l'output di un test per capire cosa esso significhi, questo è un sicuro segno che avete disegnato male i vostri test.
2 Qui c'è da apprendere una lezione simile a quella appena esposta: anche se “sappiamo” che la funzione toRoman restituisce sempre un numero romano in lettere maiuscole, convertiamo lo stesso in modo esplicito il risultato di toRoman in lettere maiuscole, per verificare che fromRoman lo accetti. Perché? Perché il fatto che toRoman restituisca sempre lettere maiuscole è un requisito indipendente. Se cambiassimo questo requisito, in modo che, ad esempio, toRoman restituisse sempre numeri romani in lettere minuscole, allora il test testToRomanCase dovrebbe essere cambiato, ma questo test funzionerebbe ancora. Questa era un'altra delle nostre regole fondamentali: ogni test deve poter funzionare isolato dagli altri. Ogni test è un isola.
3 Si noti che non assegnamo ad alcunché il valore di ritorno di fromRoman. Questo è sintatticamente corretto in Python; se una funzione restituisce un valore ma nessuno lo recepisce, Python semplicemente lo butta via. In questo caso, è quello che vogliamo. Questo test non verifica niente circa il valore di ritorno; esso verifica semplicemente che fromRoman accetti un input in lettere maiuscole senza sollevare eccezioni.

Footnotes

[13] Posso resistere a tutto, tranne che alle tentazioni.” --Oscar Wilde