14.3. roman.py, étape 3

Maintenant que toRoman se comporte correctement avec des entrées correctes (des entiers de 1 à 3999), il est temps de faire en sorte qu’il se comporte bien avec des entrées incorrectes (tout le reste).

Exemple 14.6. roman3.py

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

Si vous ne l’avez pas déjà fait, vous pouvez télécharger cet exemple ainsi que les autres exemples du livre.

"""Convert to and from Roman numerals"""

#Define exceptions
class RomanError(Exception): pass
class OutOfRangeError(RomanError): pass
class NotIntegerError(RomanError): pass
class InvalidRomanNumeralError(RomanError): pass

#Define digit mapping
romanNumeralMap = (('M',  1000),
                   ('CM', 900),
                   ('D',  500),
                   ('CD', 400),
                   ('C',  100),
                   ('XC', 90),
                   ('L',  50),
                   ('XL', 40),
                   ('X',  10),
                   ('IX', 9),
                   ('V',  5),
                   ('IV', 4),
                   ('I',  1))

def toRoman(n):
    """convert integer to Roman numeral"""
    if not (0 < n < 4000):                                             1
        raise OutOfRangeError, "number out of range (must be 1..3999)" 2
    if int(n) <> n:                                                    3
        raise NotIntegerError, "non-integers can not be converted"

    result = ""                                                        4
    for numeral, integer in romanNumeralMap:
        while n >= integer:
            result += numeral
            n -= integer
    return result

def fromRoman(s):
    """convert Roman numeral to integer"""
    pass
1 Voici un beau raccourci Pythonique : les comparaisons multiples. C’est l’équivalent de if not ((0 < n) and (n < 4000)), mais en beaucoup plus lisible. C’est notre vérification d’étendue, elle doit intercepter les entrées trop grandes, négatives ou égales à zéro.
2 Pour déclencher vous-même une exception, utilisez l’instruction raise. Vous pouvez déclencher n’importe quelle exception prédéfinie ou que vous avez défini vous-même. Le deuxième paramètre, le message d’erreur, est optionnel, il est affiché dans la trace de pile qui est affichée si l’exception n’est pas prise en charge.
3 Ceci est notre vérification de nombre décimal. Les nombres décimaux ne peuvent pas être convertis en chiffres romains.
4 Le reste de la fonction est inchangé.

Exemple 14.7. Gestion des entrées incorrectes par toRoman

>>> import roman3
>>> roman3.toRoman(4000)
Traceback (most recent call last):
  File "<interactive input>", line 1, in ?
  File "roman3.py", line 27, in toRoman
    raise OutOfRangeError, "number out of range (must be 1..3999)"
OutOfRangeError: number out of range (must be 1..3999)
>>> roman3.toRoman(1.5)
Traceback (most recent call last):
  File "<interactive input>", line 1, in ?
  File "roman3.py", line 29, in toRoman
    raise NotIntegerError, "non-integers can not be converted"
NotIntegerError: non-integers can not be converted

Exemple 14.8. Sortie de romantest3.py avec roman3.py

fromRoman should only accept uppercase input ... FAIL
toRoman should always return uppercase ... ok
fromRoman should fail with malformed antecedents ... FAIL
fromRoman should fail with repeated pairs of numerals ... FAIL
fromRoman should fail with too many repeated numerals ... FAIL
fromRoman should give known result with known input ... FAIL
toRoman should give known result with known input ... ok 1
fromRoman(toRoman(n))==n for all n ... FAIL
toRoman should fail with non-integer input ... ok        2
toRoman should fail with negative input ... ok           3
toRoman should fail with large input ... ok
toRoman should fail with 0 input ... ok
1 toRoman passe toujours le test des valeurs connues, ce qui est réconfortant. Tous les tests qui passaient à l’étape 2 passent toujours, donc notre nouveau code n’a rien endommagé.
2 Plus enthousiasmant, maintenant notre test de valeurs incorrectes passe. Ce test, testDecimal, passe grâce à la vérification int(n) <> n. Lorsqu’un nombre décimal est passé à toRoman, int(n) <> n le voit et déclenche l’exception NotIntegerError, qui est ce que testDecimalattend.
3 Ce test, testNegative, passe grâce à la vérification not (0 < n < 4000), qui déclenche une exception OutOfRangeError, qui est ce que testNegative attend.

======================================================================
FAIL: fromRoman should only accept uppercase input
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\docbook\dip\py\roman\stage3\romantest3.py", line 156, in testFromRomanCase
    roman3.fromRoman, numeral.lower())
  File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
    raise self.failureException, excName
AssertionError: InvalidRomanNumeralError
======================================================================
FAIL: fromRoman should fail with malformed antecedents
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\docbook\dip\py\roman\stage3\romantest3.py", line 133, in testMalformedAntecedent
    self.assertRaises(roman3.InvalidRomanNumeralError, roman3.fromRoman, s)
  File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
    raise self.failureException, excName
AssertionError: InvalidRomanNumeralError
======================================================================
FAIL: fromRoman should fail with repeated pairs of numerals
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\docbook\dip\py\roman\stage3\romantest3.py", line 127, in testRepeatedPairs
    self.assertRaises(roman3.InvalidRomanNumeralError, roman3.fromRoman, s)
  File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
    raise self.failureException, excName
AssertionError: InvalidRomanNumeralError
======================================================================
FAIL: fromRoman should fail with too many repeated numerals
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\docbook\dip\py\roman\stage3\romantest3.py", line 122, in testTooManyRepeatedNumerals
    self.assertRaises(roman3.InvalidRomanNumeralError, roman3.fromRoman, s)
  File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
    raise self.failureException, excName
AssertionError: InvalidRomanNumeralError
======================================================================
FAIL: fromRoman should give known result with known input
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\docbook\dip\py\roman\stage3\romantest3.py", line 99, in testFromRomanKnownValues
    self.assertEqual(integer, result)
  File "c:\python21\lib\unittest.py", line 273, in failUnlessEqual
    raise self.failureException, (msg or '%s != %s' % (first, second))
AssertionError: 1 != None
======================================================================
FAIL: fromRoman(toRoman(n))==n for all n
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\docbook\dip\py\roman\stage3\romantest3.py", line 141, in testSanity
    self.assertEqual(integer, result)
  File "c:\python21\lib\unittest.py", line 273, in failUnlessEqual
    raise self.failureException, (msg or '%s != %s' % (first, second))
AssertionError: 1 != None
----------------------------------------------------------------------
Ran 12 tests in 0.401s

FAILED (failures=6) 1
1 Nous n’en sommes plus qu’à 6 échecs, tous ayant trait à fromRoman : le test de valeurs connues, les trois tests de valeurs incorrectes, le test de casse et le test de cohérence. Cela signifie que toRoman a passé tous les tests qu’il peut passer par lui-même. (Il joue un rôle dans le test de cohérence, mais ce test à également besoin de fromRoman, qui n’est pas encore écrit.) Cela veut dire que nous devons arrêter d’écrire le code de toRoman immédiatement. Pas de réglages, pas de bidouilles et pas de vérification supplémentaires «au cas où». Arrêtez. Maintenant. Ecartez vous du clavier.
NOTE
La chose la plus importante que des tests unitaires complets vous disent est quand vous arrêter d’écrire du code. Quand tous les tests unitaires d’une fonction passent, arrêtez d’écrire le code de la fonction. Quand tous les tests d’un module passent, arrêtez d’écrire le code du module.