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.
"""Convert to and from Roman numerals"""#Define exceptionsclass RomanError(Exception): passclass OutOfRangeError(RomanError): passclass NotIntegerError(RomanError): passclass 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"""ifnot (0 < n < 4000): raise OutOfRangeError, "number out of range (must be 1..3999)"if int(n) <> n: raise NotIntegerError, "non-integers can not be converted"
result = ""for numeral, integer in romanNumeralMap:
while n >= integer:
result += numeral
n -= integer
return result
def fromRoman(s):
"""convert Roman numeral to integer"""pass
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.
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.
Ceci est notre vérification de nombre décimal. Les nombres
décimaux ne peuvent pas être convertis en chiffres romains.
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
fromRoman(toRoman(n))==n for all n ... FAIL
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
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é.
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.
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)
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.
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.