Chapitre 15. Refactorisation

15.1. Gestion des bogues

Malgré tous vos efforts pour écrire des tests unitaires exhaustifs, vous aurez à faire face à des bogues. Mais qu’est-ce que je veux dire par «bogue» ? Un bogue est un cas de test que vous n’avez pas encore écrit.

Exemple 15.1. Le bogue

>>> import roman5
>>> roman5.fromRoman("") 1
0
1 Vous vous rappelez que dans la section précédente nous avons vu à chaque fois qu’une chaîne vide était reconnue par l’expression régulière que nous utilisons pour vérifier la validité des nombres romains. En fait, c’est toujours vrai pour la version finale de l’expression régulière. Et c’est un bogue, nous voulons qu’une chaîne vide déclenche une exception InvalidRomanNumeralError comme toute autre séquence de caractères qui ne représente pas un nombre romain valide.

Après avoir reproduit le bogue et avant de le corriger, vous devez écrire un cas de test qui échoue, de manière à l’illustrer.

Exemple 15.2. Test du bogue (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 C’est plutôt simple. On appelle fromRoman avec une chaîne vide et on s’assure qu’un exception InvalidRomanNumeralError est déclenchée. Le plus dur était de trouver le bogue, maintenant qu’on le connaît, le tester est facile.

Puisque notre code a un bogue et que nous avons maintenant un cas de test pour ce bogue, le cas de test va échouer :

Exemple 15.3. Sortie de romantest61.py avec 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)

Maintenant nous pouvons corriger le bogue.

Exemple 15.4. Correction du bogue (roman62.py)

Ce fichier est disponible dans le sous-répertoire py/roman/stage6/ du répertoire des exemples.


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 Seulement deux lignes de code sont nécessaires : une vérification explicite de chaîne non nulle et une instruction raise.

Exemple 15.5. Sortie de romantest62.py avec 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 Le cas de test pour la chaîne vide passe maintenant, le bogue est donc corrigé.
2 Les autres cas de test passent toujours, ce qui veut dire que la correction du bogue n’a pas endommagé d’autre code. Tous les tests passent, on arrête d’écrire du code.

Programmer de cette manière ne rend pas la correction de bogues plus simple. Les bogues simples (comme ici) nécessitent des cas de tests simples, les bogues complexes de cas de tests complexes. Dans un environnement centré sur les tests, il peut sembler que la correction d’un bogue prend plus de temps puisque vous devez définir exactement par du code ce qu’est le bogue (pour écrire le cas de test) avant de corriger le bogue proprement dit. Puis, si le cas de test ne passe pas immédiatement, vous devez déterminer si la correction est erronée ou si le cas de test a lui-même un bogue. Cependant, à terme, ces aller-retours entre le code de test et le code testé est rentable car il rend plus probable la correction des bogues du premier coup. De plus, puisque vous pouvez facilement lancer tous les cas de tests en même temps que le nouveau, vous êtes beaucoup moins susceptibles d’endommager une partie de l’ancien code en corrigeant le nouveau. Les tests unitaires d’aujourd’hui sont les tests de non régression de demain.