Maintenant que nos tests unitaires sont complets, il est temps
d’écrire le code que nos cas de test essaient de tester. Nous allons
faire cela par étapes, de manière à voir tous les cas échouer, puis à
les voir passer un par un au fur et à mesure que nous remplissons les
trous de roman.py.
Exemple 14.1. roman1.py
Ce fichier est disponible dans le sous-répertoire py/roman/stage1/ du répertoire des exemples.
"""Convert to and from Roman numerals"""#Define exceptionsclass RomanError(Exception): pass
class OutOfRangeError(RomanError): pass
class NotIntegerError(RomanError): passclass InvalidRomanNumeralError(RomanError): passdef toRoman(n):
"""convert integer to Roman numeral"""passdef fromRoman(s):
"""convert Roman numeral to integer"""pass
C’est de cette manière que l’on défini ses propres
exceptions en Python. Les exceptions
sont des classes, on en crée de nouvelles en dérivant des
exceptions existantes. Il est fortement recommandé (mais pas
obligatoire) de dériver Exception, qui est
la classe de base dont toutes les exceptions héritent. Ici, je
définis RomanError (dérivée de
Exception) comme classe de base de toutes
mes autres exceptions à venir. C’est une question de style,
j’aurais tout aussi bien pu dériver chaque exception directement
de la classe Exception.
Les exceptions OutOfRangeError et
NotIntegerError seront utilisées plus tard
par toRoman pour signaler diverses sortes
d’entrées invalides, tel que spécifié par ToRomanBadInput.
L’exception InvalidRomanNumeralError
sera utilisée plus tard par fromRoman pour
signaler une entrée invalide, comme spécifié par FromRomanBadInput.
A cette étape, nous voulons définir
l’API de chacune de nos fonctions, mais nous ne
voulons pas encore en écrire le code, nous les mettons donc en
place à l’aide du mot réservé Pythonpass.
Et maintenant, l’instant décisif (roulement de tambour) : nous
allons exécuter notre test unitaire avec cette ébauche de module. A ce
niveau, chaque cas de test devrait échouer. En fait, si un cas de test
passe à l’étape 1, il faut retourner à romantest.py
et rechercher comment nous avons écrit un test inutile au point de
passer avec des fonctions ne faisant rien.
Exécutez romantest1.py avec l’option de ligne
de commande -v, qui donne une sortie plus détaillée
pour voir exactement ce qui se passe à mesure que chaque test s’exécute.
Si tout se passe bien, votre sortie devrait ressembler à ceci :
Exemple 14.2. Sortie de romantest1.py avec roman1.py
fromRoman should only accept uppercase input ... ERROR
toRoman should always return uppercase ... ERROR
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 ... FAIL
fromRoman(toRoman(n))==n for all n ... FAIL
toRoman should fail with non-integer input ... FAIL
toRoman should fail with negative input ... FAIL
toRoman should fail with large input ... FAIL
toRoman should fail with 0 input ... FAIL
======================================================================
ERROR: fromRoman should only accept uppercase input
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 154, in testFromRomanCase
roman1.fromRoman(numeral.upper())
AttributeError: 'None' object has no attribute 'upper'
======================================================================
ERROR: toRoman should always return uppercase
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 148, in testToRomanCase
self.assertEqual(numeral, numeral.upper())
AttributeError: 'None' object has no attribute 'upper'
======================================================================
FAIL: fromRoman should fail with malformed antecedents
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 133, in testMalformedAntecedent
self.assertRaises(roman1.InvalidRomanNumeralError, roman1.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\stage1\romantest1.py", line 127, in testRepeatedPairs
self.assertRaises(roman1.InvalidRomanNumeralError, roman1.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\stage1\romantest1.py", line 122, in testTooManyRepeatedNumerals
self.assertRaises(roman1.InvalidRomanNumeralError, roman1.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\stage1\romantest1.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: toRoman should give known result with known input
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 93, in testToRomanKnownValues
self.assertEqual(numeral, result)
File "c:\python21\lib\unittest.py", line 273, in failUnlessEqual
raise self.failureException, (msg or '%s != %s' % (first, second))
AssertionError: I != None
======================================================================
FAIL: fromRoman(toRoman(n))==n for all n
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\docbook\dip\py\roman\stage1\romantest1.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
======================================================================
FAIL: toRoman should fail with non-integer input
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 116, in testNonInteger
self.assertRaises(roman1.NotIntegerError, roman1.toRoman, 0.5)
File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
raise self.failureException, excName
AssertionError: NotIntegerError
======================================================================
FAIL: toRoman should fail with negative input
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 112, in testNegative
self.assertRaises(roman1.OutOfRangeError, roman1.toRoman, -1)
File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
raise self.failureException, excName
AssertionError: OutOfRangeError
======================================================================
FAIL: toRoman should fail with large input
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 104, in testTooLarge
self.assertRaises(roman1.OutOfRangeError, roman1.toRoman, 4000)
File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
raise self.failureException, excName
AssertionError: OutOfRangeError
======================================================================
FAIL: toRoman should fail with 0 input
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 108, in testZero
self.assertRaises(roman1.OutOfRangeError, roman1.toRoman, 0)
File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
raise self.failureException, excName
AssertionError: OutOfRangeError
----------------------------------------------------------------------
Ran 12 tests in 0.040s
FAILED (failures=10, errors=2)
Lancer le script exécute
unittest.main(), qui exécute chaque cas de
test, c’est à dire chaque méthode de chaque classe dans
romantest.py. Pour chaque cas de test, il
affiche la doc string de la méthode et le
résultat du test. Comme il était attendu, aucun de nos cas de test
ne passe.
Pour chaque test échoué, unittest affiche la trace de pile
montrant exactement ce qui s’est passé. Dans le cas présent, notre
appel à assertRaises (appelé aussi
failUnlessRaises) a déclenché une exception
AssertionError car il s’attendait à ce que
toRoman déclenche une exception
OutOfRangeError, ce qui ne s’est pas
produit.
Après le détail, unittest affiche en résumé le nombre
de tests réalisés et le temps que cela a pris.
Le test unitaire dans son ensemble a échoué puisqu’au moins
un cas de test n’est pas passé. Lorsqu’un cas de test ne passe
pas, unittest distingue
les échecs des erreurs. Un échec est un appel à une méthode
assertXYZ, comme assertEqual ou
assertRaises, qui échoue parce que la
condition de l’assertion n’est pas vraie ou que l’exception
attendue n’a pas été déclenchée. Une erreur est tout autre sorte
d’exception déclenchée dans le code que l’on teste ou dans le test
unitaire lui-même. Par exemple, la méthode
testFromRomanCase
(«fromRoman doit seulement accepter une
entrée en majuscules») provoque une erreur parce que
l’appel à numeral.upper() déclenche une
exception AttributeError,
toRoman étant supposé retourner une chaîne
mais ne l’ayant pas fait. Mais testZero
(«fromRoman doit échouer avec 0 en
entrée») provoque un échec parce que l’appel à
fromRoman n’a pas déclenché l’exception
InvalidRomanNumeral que
assertRaises attendait.