7.11. Come gestire gli errori di programmazione

A dispetto dei nostri migliori sforzi per scrivere test completi per le unità di codice, capita di fare degli errori di programmazione, in gergo chiamati bachi (“bug”). Un baco corrisponde ad un test che non è stato ancora scritto.

Esempio 7.22. Il baco

>>> import roman5
>>> roman5.fromRoman("") 1
0
1 Ricordate nella sezione precedente quando continuavamo a dire che una stringa vuota corrispondeva con l'espressione regolare che usavamo per controllare un numero romano valido? Bene, capita che questo sia ancora valido per la versione finale dell'espressione regolare. Questo è un baco; noi vogliamo che una stringa vuota sollevi un'eccezione InvalidRomanNumeralError esattamente come ogni altra sequenza di caratteri che non rappresenta un numero romano valido.

Dopo aver ricostruito il baco, e prima di porvi rimedio, è opportuno scrivere un test che fallisca a causa del baco, illustrandone così le caratteristiche.

Esempio 7.23. Verificare la presenza del baco (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
1 La cosa è piuttosto semplice. Basta chiamare la funzione fromRoman con una stringa vuota e assicurarsi che sollevi un eccezione InvalidRomanNumeralError. La parte difficile è stata individuare il baco, adesso che ne conosciamo la presenza, scrivere un test per verificarlo è semplice.

Dato che il nostro codice ha un baco, e noi abbiamo un test che verifica questo baco, il test fallirà.

Esempio 7.24. Output di romantest61.py a fronte di 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)

Ora possiamo risolvere il problema.

Esempio 7.25. Eliminazione del baco (roman62.py)


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
1 Solo due linee di codice sono richieste: un controllo esplicito per la striga vuota ed un' istruzione raise.

Esempio 7.26. Output di romantest62.py a fronte di 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
1 Il test per la stringa vuota ora passa, quindi il baco è stato eliminato.
2 Tutti gli altri test continuano a passare con successo e questo significa che l'eliminazione del baco non ha scombinato nient'altro. Smettete di scrivere codice.

Scrivere codice in questo modo non serve a rendere più facile l'eliminazione dei bachi. Bachi semplici (come questo) richiedono test semplici; casi più complessi richiedono test più complessi. In un ambiente orientato ai test, può sembrare che ci voglia più tempo per eliminare un baco, giacché è necessario dettagliare in forma di codice esattamente qual'è il baco (per scrivere il test), e poi eliminare il problema. Quindi, se il test non passa subito con successo, occorre scoprire se ad essere sbagliata sia la correzione del problema o il test. Tuttavia, alla distanza, questo andare avanti e indietro tra il codice da verificare ed il codice di test ripaga, perché rende più probabile che un baco sia eliminato correttamente al primo colpo. Inoltre, dato che è possibile eseguire i vecchi test insieme ai i nuovi, è molto meno probabile che si danneggi il codice esistente quando si cerca di eliminare il baco. I test delle unità di codice di oggi saranno i test di regressione di domani.