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("")
0
|
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):
def testBlank(self):
"""fromRoman should fail with blank string"""
self.assertRaises(roman.InvalidRomanNumeralError, roman.fromRoman, "")
|
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:
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
|
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
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 
|
Le cas de test pour la chaîne vide passe maintenant, le
bogue est donc corrigé.
|
|
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.