XIII. Tests unitaires ▲
XIII-A. Introduction au chiffres romains▲
Dans les chapitres précédents, nous avons «plongé» en regardant immédiatement du code et en essayant de le comprendre le plus vite possible. Maintenant que vous connaissez un peu plus de Python, nous allons prendre un peu de recul et regarder ce qui se passe avant que le code soit écrit.
Dans ce chapitres et les suivants, nous allons écrire, déboguer et optimiser un ensemble de fonctions utilitaires pour convertir vers et depuis des chiffres romains. Nous avons vu la méthode de construction et de validation des chiffres romains au chapitre Section 7.3, «Exemple : chiffres romains», nous allons maintenant considérer ce qu'il faut faire pour étendre cette méthode pour qu'elle fonctionne dans les deux sens.
Les règles de construction des chiffres romains amènent à un certain nombre d'observations intéressantes :
- Il n'y a qu'une seule façon correcte de représenter une valeur en chiffres romains.
- L'inverse est aussi vrai : si une chaîne de caractères en chiffres romains est un nombre valide, elle ne représente qu'un nombre (c.a.d. qu'elle ne peut être lue que d'une manière).
- Il y a un intervalle limité de valeurs pouvant être exprimées en chiffres romains, les nombres de 1 à 3999 (les Romains avaient plusieurs manières d'exprimer des nombres plus grand, par exemple en inscrivant une barre au dessus d'un caractère pour signifier que sa valeur normale devait être multipliée par 1000, mais nous n'allons pas prendre ça en compte. Pour ce qui est de ce chapitre, les chiffres romains vont de 1 à 3999).
- Il n'est pas possible de représenter 0 en chiffres romains (étonnamment, les anciens romains n'avaient pas de notion du 0 comme chiffre. Les nombres servaient à compter les choses qu'on avait, comment compter ce que l'on n'a pas ?).
- Il n'est pas possible de représenter les valeurs négatives en chiffres romains.
- Il n'est pas possible de représenter des fractions ou des nombres non-entiers en chiffres romains.
Sachant tout cela, que pouvons nous exiger d'un ensemble de fonctions pour convertir vers et depuis les chiffres romains ?
Spécification de roman.py
- toRoman doit retourner la représentation en chiffres romains de tous les entiers entre 1 et 3999.
- toRoman doit échouer s'il lui est passé un entier hors de l'intervalle 1 à 3999.
- toRoman doit échouer s'il lui est passé une valeur non-entière.
- fromRoman doit prendre un nombre en chiffres romains valide et retourner la valeur qu'il représente.
- fromRoman doit échouer s'il lui est passé un nombre en chiffres romains invalide.
- Si vous prenez un nombre, le convertissez en chiffres romains, puis le convertissez à nouveau en nombre, vous devez obtenir la même valeur que celle de départ. Donc fromRoman(toRoman(n)) == n pour tout n compris dans 1..3999.
- toRoman doit toujours retourner un des chiffres romains en lettres majuscules.
- fromRoman doit seulement accepter des chiffres romains en majuscules (il doit échouer s'il lui est passé une entrée en minuscules.
Pour en savoir plus
XIII-B. Présentation de romantest.py▲
Maintenant que nous avons défini entièrement le comportement que nous attendons de nos fonctions de conversion, nous allons faire quelque chose d'un peu inattendu : nous allons écrire une suite de tests qui évalue ces fonctions et s'assure qu'elle se comporte comme nous voulons qu'elles le fassent. Vous avez bien lu, nous allons écrire du code pour tester du code que nous n'avons pas encore écrit.
C'est ce qu'on appelle des tests unitaires (unit test), puisque l'ensemble des deux fonctions de conversion peut être écrit et testé comme une unité, séparée de tout programme plus grand dont elle puisse faire partie plus tard. Python a une bibliothèque pour les tests unitaires, un module nommé tout simplement unittest.
unittest est inclus dans Python 2.1 et versions ultérieures. Les utilisateurs de Python 2.0 peuvent le télécharger depuis pyunit.sourceforge.net.
Les tests unitaires sont une partie importante d'une stratégie générale de développement centrée sur les tests. Si vous écrivez des tests unitaires, il est important de les écrire tôt (de préférence avant d'écrire le code qu'ils testent) et de les maintenir à jour au fur et à mesure que le code et les spécifications changent. Les tests unitaires ne remplacent pas les tests fonctionnels ou de système à plus haut niveau, mais ils sont important dans toutes les phases de développement
- Avant d'écrire le code, ils obligent a préciser le détail des spécification d'une manière utile.
- Pendant l'écriture du code, ils empêchent de trop programmer. Quand tous les cas de test passent, la fonction est terminée.
- Pendant la refactorisation (refactoring) de code, ils garantissent que la nouvelle version se comporte comme l'ancienne.
- Pendant la maintenance du code, ils permettent d'être couvert si quelqu'un vient hurler que votre dernière modification fait planter leur code. («Les tests unitaires passaient à l'intégration de mon code...»)
- Lorsqu'on écrit du code en équipe, ils permettent de s'assurer que le code que vous intégrez ne va pas interférer avec celui des autres puisque vous pouvez d'abord exécuter leurs tests. J'ai vu ce genre de chose au cours de code sprints. L'équipe se partage les tâches, chacun écrit les tests unitaires pour sa tâche à partir de sa spécification puis partage ses tests avec le reste de l'équipe. De cette manière, personne ne peut s'égarer à écrire du code qui ne fonctionnera pas avec celui des autres.
XIII-C. Présentation de romantest.py▲
Voici la suite de tests complète de nos fonctions de conversion de chiffres romains, qui n'ont pas encore été écrites mais le seront dans roman.py. La manière dont tout ça fonctionne ensemble n'est pas immédiatement évidente, aucune de ces classes ou méthodes ne se référencent entre elles. Il y a de bonnes raisons à cela, comme nous le verrons bientôt.
Exemple 13.1. romantest.py▲
Si vous ne l'avez pas déjà fait,vous pouvez télécharger cet exemple ainsi que les autres exemples du livre.
"""Unit test for roman.py"""
import
roman
import
unittest
class
KnownValues
(
unittest.TestCase):
knownValues =
(
(
1
, 'I'
),
(
2
, 'II'
),
(
3
, 'III'
),
(
4
, 'IV'
),
(
5
, 'V'
),
(
6
, 'VI'
),
(
7
, 'VII'
),
(
8
, 'VIII'
),
(
9
, 'IX'
),
(
10
, 'X'
),
(
50
, 'L'
),
(
100
, 'C'
),
(
500
, 'D'
),
(
1000
, 'M'
),
(
31
, 'XXXI'
),
(
148
, 'CXLVIII'
),
(
294
, 'CCXCIV'
),
(
312
, 'CCCXII'
),
(
421
, 'CDXXI'
),
(
528
, 'DXXVIII'
),
(
621
, 'DCXXI'
),
(
782
, 'DCCLXXXII'
),
(
870
, 'DCCCLXX'
),
(
941
, 'CMXLI'
),
(
1043
, 'MXLIII'
),
(
1110
, 'MCX'
),
(
1226
, 'MCCXXVI'
),
(
1301
, 'MCCCI'
),
(
1485
, 'MCDLXXXV'
),
(
1509
, 'MDIX'
),
(
1607
, 'MDCVII'
),
(
1754
, 'MDCCLIV'
),
(
1832
, 'MDCCCXXXII'
),
(
1993
, 'MCMXCIII'
),
(
2074
, 'MMLXXIV'
),
(
2152
, 'MMCLII'
),
(
2212
, 'MMCCXII'
),
(
2343
, 'MMCCCXLIII'
),
(
2499
, 'MMCDXCIX'
),
(
2574
, 'MMDLXXIV'
),
(
2646
, 'MMDCXLVI'
),
(
2723
, 'MMDCCXXIII'
),
(
2892
, 'MMDCCCXCII'
),
(
2975
, 'MMCMLXXV'
),
(
3051
, 'MMMLI'
),
(
3185
, 'MMMCLXXXV'
),
(
3250
, 'MMMCCL'
),
(
3313
, 'MMMCCCXIII'
),
(
3408
, 'MMMCDVIII'
),
(
3501
, 'MMMDI'
),
(
3610
, 'MMMDCX'
),
(
3743
, 'MMMDCCXLIII'
),
(
3844
, 'MMMDCCCXLIV'
),
(
3888
, 'MMMDCCCLXXXVIII'
),
(
3940
, 'MMMCMXL'
),
(
3999
, 'MMMCMXCIX'
))
def
testToRomanKnownValues
(
self):
"""toRoman should give known result with known input"""
for
integer, numeral in
self.knownValues:
result =
roman.toRoman
(
integer)
self.assertEqual
(
numeral, result)
def
testFromRomanKnownValues
(
self):
"""fromRoman should give known result with known input"""
for
integer, numeral in
self.knownValues:
result =
roman.fromRoman
(
numeral)
self.assertEqual
(
integer, result)
class
ToRomanBadInput
(
unittest.TestCase):
def
testTooLarge
(
self):
"""toRoman should fail with large input"""
self.assertRaises
(
roman.OutOfRangeError, roman.toRoman, 4000
)
def
testZero
(
self):
"""toRoman should fail with 0 input"""
self.assertRaises
(
roman.OutOfRangeError, roman.toRoman, 0
)
def
testNegative
(
self):
"""toRoman should fail with negative input"""
self.assertRaises
(
roman.OutOfRangeError, roman.toRoman, -
1
)
def
testNonInteger
(
self):
"""toRoman should fail with non-integer input"""
self.assertRaises
(
roman.NotIntegerError, roman.toRoman, 0.5
)
class
FromRomanBadInput
(
unittest.TestCase):
def
testTooManyRepeatedNumerals
(
self):
"""fromRoman should fail with too many repeated numerals"""
for
s in
(
'MMMM'
, 'DD'
, 'CCCC'
, 'LL'
, 'XXXX'
, 'VV'
, 'IIII'
):
self.assertRaises
(
roman.InvalidRomanNumeralError, roman.fromRoman, s)
def
testRepeatedPairs
(
self):
"""fromRoman should fail with repeated pairs of numerals"""
for
s in
(
'CMCM'
, 'CDCD'
, 'XCXC'
, 'XLXL'
, 'IXIX'
, 'IVIV'
):
self.assertRaises
(
roman.InvalidRomanNumeralError, roman.fromRoman, s)
def
testMalformedAntecedent
(
self):
"""fromRoman should fail with malformed antecedents"""
for
s in
(
'IIMXCC'
, 'VX'
, 'DCM'
, 'CMM'
, 'IXIV'
,
'MCMC'
, 'XCX'
, 'IVI'
, 'LM'
, 'LD'
, 'LC'
):
self.assertRaises
(
roman.InvalidRomanNumeralError, roman.fromRoman, s)
class
SanityCheck
(
unittest.TestCase):
def
testSanity
(
self):
"""fromRoman(toRoman(n))==n for all n"""
for
integer in
range(
1
, 4000
):
numeral =
roman.toRoman
(
integer)
result =
roman.fromRoman
(
numeral)
self.assertEqual
(
integer, result)
class
CaseCheck
(
unittest.TestCase):
def
testToRomanCase
(
self):
"""toRoman should always return uppercase"""
for
integer in
range(
1
, 4000
):
numeral =
roman.toRoman
(
integer)
self.assertEqual
(
numeral, numeral.upper
(
))
def
testFromRomanCase
(
self):
"""fromRoman should only accept uppercase input"""
for
integer in
range(
1
, 4000
):
numeral =
roman.toRoman
(
integer)
roman.fromRoman
(
numeral.upper
(
))
self.assertRaises
(
roman.InvalidRomanNumeralError,
roman.fromRoman, numeral.lower
(
))
if
__name__
==
"__main__"
:
unittest.main
(
)
Pour en savoir plus
- Le site Web de PyUnit présente un traitement en profondeur de l'usage du module unittest, y compris des fonctionnalités avancées non couvertes par ce chapitre.
- La FAQ PyUnit explique pourquoi les cas de test sont stockés séparément du code qu'ils testent.
- La Python Library Reference résume le module unittest.
- ExtremeProgramming.org explique pourquoi vous devriez écrire des tests unitaires.
- Le Portland Pattern Repository propose une discussion en cours sur les tests unitaires, y compris une définition standard, pourquoi vous devriez écrire les tests unitaires en premier et de nombreuses études de cas en profondeur.
XIII-D. Tester la réussite▲
La partie fondamentale des tests unitaires est la construction des cas de test individuels. Un cas de test répond à une seule question à propos du code qu'il teste.
Un cas de test doit pouvoir :
- être exécuté complètement seul, sans entrée humaine. Les tests unitaires sont une question d'automatisation.
- déterminer lui-même si la fonction qu'il teste passe ou échoue au test, sans interprétation humaine du résultat.
- être exécuté de manière isolée, séparée de tout autre cas de test (même concernant la même fonction). Chaque cas de test est une île.
Sachant cela, construisons notre premier cas de test. Nous avons la spécification suivante :
- toRoman doit retourner la représentation en chiffres romains de tous les entiers entre 1 et 3999.
Exemple 13.2. testToRomanKnownValues▲
class
KnownValues
(
unittest.TestCase): ***
1
***
knownValues =
(
(
1
, 'I'
),
(
2
, 'II'
),
(
3
, 'III'
),
(
4
, 'IV'
),
(
5
, 'V'
),
(
6
, 'VI'
),
(
7
, 'VII'
),
(
8
, 'VIII'
),
(
9
, 'IX'
),
(
10
, 'X'
),
(
50
, 'L'
),
(
100
, 'C'
),
(
500
, 'D'
),
(
1000
, 'M'
),
(
31
, 'XXXI'
),
(
148
, 'CXLVIII'
),
(
294
, 'CCXCIV'
),
(
312
, 'CCCXII'
),
(
421
, 'CDXXI'
),
(
528
, 'DXXVIII'
),
(
621
, 'DCXXI'
),
(
782
, 'DCCLXXXII'
),
(
870
, 'DCCCLXX'
),
(
941
, 'CMXLI'
),
(
1043
, 'MXLIII'
),
(
1110
, 'MCX'
),
(
1226
, 'MCCXXVI'
),
(
1301
, 'MCCCI'
),
(
1485
, 'MCDLXXXV'
),
(
1509
, 'MDIX'
),
(
1607
, 'MDCVII'
),
(
1754
, 'MDCCLIV'
),
(
1832
, 'MDCCCXXXII'
),
(
1993
, 'MCMXCIII'
),
(
2074
, 'MMLXXIV'
),
(
2152
, 'MMCLII'
),
(
2212
, 'MMCCXII'
),
(
2343
, 'MMCCCXLIII'
),
(
2499
, 'MMCDXCIX'
),
(
2574
, 'MMDLXXIV'
),
(
2646
, 'MMDCXLVI'
),
(
2723
, 'MMDCCXXIII'
),
(
2892
, 'MMDCCCXCII'
),
(
2975
, 'MMCMLXXV'
),
(
3051
, 'MMMLI'
),
(
3185
, 'MMMCLXXXV'
),
(
3250
, 'MMMCCL'
),
(
3313
, 'MMMCCCXIII'
),
(
3408
, 'MMMCDVIII'
),
(
3501
, 'MMMDI'
),
(
3610
, 'MMMDCX'
),
(
3743
, 'MMMDCCXLIII'
),
(
3844
, 'MMMDCCCXLIV'
),
(
3888
, 'MMMDCCCLXXXVIII'
),
(
3940
, 'MMMCMXL'
),
(
3999
, 'MMMCMXCIX'
)) ***
2
***
def
testToRomanKnownValues
(
self): ***
3
***
"""toRoman should give known result with known input"""
for
integer, numeral in
self.knownValues:
result =
roman.toRoman
(
integer) ***
4
***
***
5
***
self.assertEqual
(
numeral, result) ***
6
***
***1*** Pour écrire un cas de test, commencez par dériver de la classe TestCase du module unittest. Cette classe fournit de nombreuses méthodes utiles que vous pouvez utiliser dans vos cas de test pour tester de conditions spécifiques.
***2*** Voici un liste de paires entier/romains que j'ai vérifié manuellement. Elle comprend les dix plus petits nombres, le plus grand nombre, chaque nombre représenté par un seul caractère en chiffres romains et un échantillon aléatoire d'autres nombres valides. Le but d'un test unitaire n'est pas de tester toutes les entrées possibles, mais d'en tester un échantillon représentatif.
***3*** Chaque test individuel est sa propre méthode, qui ne doit prendre aucun paramètre et ne retourner aucune valeur. Si la méthode sort normalement sans déclencher d'exception, le test on considère que le test est passé, si la méthode déclenche une exception, on considère que le test a échoué.
***4*** Ici, nous appelons la véritable fonction toRoman (pour l'instant la fonction n'a pas encore été écrite, mais quand elle le sera, c'est cette ligne qui l'appellera). Remarquez que nous avons maintenant défini l'interface de la fonction toRoman : elle doit prendre un entier en paramètre (le nombre à convertir) et renvoyer une chaîne (les chiffres romains). Si l'interface est différente de ça, on considère que le test a échoué.
***5*** Remarquez également que nous ne tentons d'intercepter aucune exception quand nous appelons toRoman. C'est intentionnel. toRoman ne devrait pas déclencher d'exception lorsque nous l'appelons avec des paramètres d'entrée valides et ces valeurs sont toutes valides. Si toRoman déclenche une exception, on considère que le test a échoué.
***6*** En supposant que la fonction toRoman a été définie correctement, appelée correctement, qu'elle s'est terminée avec succès et qu'elle a retourné une valeur, la dernière étape et de vérifier qu'elle a retourné la bonne valeur. C'est une question courante et la classe TestCase fournit une méthode, assertEqual, pour vérifier si deux valeurs sont égales. Si le résultat retourné par toRoman (result) ne correspond pas à la valeur connue que nous attendions (numeral), assertEqual déclenche une exception et le test échoue. Si les deux valeurs sont égales, assertEqual ne fera rien. Si chaque valeur retourné par toRoman correspond à la valeur connue que nous attendons, assertEqual ne déclenchera jamais d'exception, donc testToRomanKnownValues se terminera finalement normalement, ce qui signifie que toRoman a passé ce test.
XIII-E. Tester l'échec▲
Rappelez-vous nos autres spécifications pour toRoman :
- 2. toRoman doit échouer s'il lui est passé un entier hors de l'intervalle 1 à 3999.
- 3. toRoman doit échouer s'il lui est passé une valeur non-entière.
En Python, les fonctions indiquent l'échec en déclenchant des exceptions et le module unittest fournit des méthodes pour tester si une fonction déclenche une exception en particulier lorsqu'on lui donne une entrée incorrecte.
Exemple 13.3. Test des entrées incorrectes pour toRoman▲
class
ToRomanBadInput
(
unittest.TestCase):
def
testTooLarge
(
self):
"""toRoman should fail with large input"""
self.assertRaises
(
roman.OutOfRangeError, roman.toRoman, 4000
) ***
1
***
def
testZero
(
self):
"""toRoman should fail with 0 input"""
self.assertRaises
(
roman.OutOfRangeError, roman.toRoman, 0
) ***
2
***
def
testNegative
(
self):
"""toRoman should fail with negative input"""
self.assertRaises
(
roman.OutOfRangeError, roman.toRoman, -
1
)
def
testNonInteger
(
self):
"""toRoman should fail with non-integer input"""
self.assertRaises
(
roman.NotIntegerError, roman.toRoman, 0.5
) ***
3
***
***1*** La classe TestCase de unittest fournit la méthode assertRaises, qui prend les arguments suivants : l'exception attendue, la fonction testée et les arguments à passer à cette fonction. (Si la fonction testée prend plus d'un argument, passez-les tous à assertRaises, dans l'ordre, qui les passera à la fonction.) Faites bien attention à ce que nous faisons ici : au lieu d'appeler la fonction toRoman directement et de vérifier manuellement qu'elle déclenche une exception particulière (en l'entourant d'un bloc try...except), assertRaises encapsule tout ça pour nous. Tout ce que nous faisons est de lui donner l'exception (roman.OutOfRangeError), la fonction (toRoman) et les arguments de toRoman (4000) et assertRaises s'occupe d'appeler toRoman et de vérifier qu'elle décleche l'exception roman.OutOfRangeError. (Est-ce que j'ai dit récemment comme il est pratique que tout en Python est un objet, y compris les fonctions et les exceptions ?)
***2*** En plus de tester les nombres trop grand, nous devons tester les nombres trop petits. Rappelez-vous, les chiffres romains ne peuvent exprimer 0 ou des valeurs négatives, donc nous avons un cas de test pour chacun (testZero et testNegative). Dans testZero, nous testons que toRoman déclenche une exception roman.OutOfRangeError lorsqu'on l'appelle avec 0, si l'exception roman.OutOfRangeError n'est pas déclenchée (soit parce qu'une valeur est retournée, soit parce qu'une autre exception est déclenchée), le test est considéré comme ayant échoué.
***3*** La spécification n°3 précise que toRoman ne peut accepter de non-entier, nous testons donc ici le déclenchement d'une exception roman.NotIntegerError lorsque toRoman est appelée avec un nombre décimal (0.5). Si toRoman ne déclenche pas l'exception roman.NotIntegerError, les test est considéré comme ayant échoué.
Les deux spécifications suivantes sont similaires aux trois premières, excepté le fait qu'elles s'appliquent à fromRoman au lieu de fromRoman :
- 4. fromRoman doit prendre un nombre en chiffres romains valide et retourner la valeur qu'il représente.
- 5. fromRoman doit échouer s'il lui est passé un nombre romain invalide.
La spécification n°4 est prise en charge de la même manière que la spécification n°1, en parcourant un échantillon de valeurs connues et en les testant une à une. La spécification n°5 est prise en charge de la même manière que les spécifications n°2 et 3, en testant une série d'entrées incorrectes et en s'assurant que fromRoman déclenche l'exception appropriée.
Exemple 13.4. Test des entrées incorrectes pour fromRoman▲
class
FromRomanBadInput
(
unittest.TestCase):
def
testTooManyRepeatedNumerals
(
self):
"""fromRoman should fail with too many repeated numerals"""
for
s in
(
'MMMM'
, 'DD'
, 'CCCC'
, 'LL'
, 'XXXX'
, 'VV'
, 'IIII'
):
self.assertRaises
(
roman.InvalidRomanNumeralError, roman.fromRoman, s) ***
1
***
def
testRepeatedPairs
(
self):
"""fromRoman should fail with repeated pairs of numerals"""
for
s in
(
'CMCM'
, 'CDCD'
, 'XCXC'
, 'XLXL'
, 'IXIX'
, 'IVIV'
):
self.assertRaises
(
roman.InvalidRomanNumeralError, roman.fromRoman, s)
def
testMalformedAntecedent
(
self):
"""fromRoman should fail with malformed antecedents"""
for
s in
(
'IIMXCC'
, 'VX'
, 'DCM'
, 'CMM'
, 'IXIV'
,
'MCMC'
, 'XCX'
, 'IVI'
, 'LM'
, 'LD'
, 'LC'
):
self.assertRaises
(
roman.InvalidRomanNumeralError, roman.fromRoman, s)
***1*** Il n'y a pas grand chose de nouveau à dire, c'est la même méthode que celle que nous avons employé pour tester les entrées incorrectes pour toRoman. Je mentionne juste qu'il y a une nouvelle exception : roman.InvalidRomanNumeralError. Cela fait un total de trois exceptions personnalisées à définir dans roman.py (avec roman.OutOfRangeError et roman.NotIntegerError). Nous verrons comment définir ces exceptions quand nous commenceront vraiment l'écriture de roman.py au chapitre suivant.
XIII-F. Tester la cohérence▲
Il est fréquent qu'une unité de code contiennent un ensemble de fonctions réciproques, habituellement sous la forme de fonctions de conversion où l'une converti de A à B et l'autre de B à A. Dans ce cas, il est utile de créer un test de cohérence pour s'assurer qu'une conversion de A à B puis de B à A n'introduit pas de perte de précision décimale, d'erreurs d'arrondi ou d'autres bogues.
Considérez cette spécification :
- 6. Si vous prenez un nombre, le convertissez en chiffres romains, puis le convertissez à nouveau en nombre, vous devez obtenir la même valeur que celle de départ. Donc fromRoman(toRoman(n)) == n pour tout n compris dans 1..3999.
Exemple 13.5. Test de toRoman et fromRoman▲
class
SanityCheck
(
unittest.TestCase):
def
testSanity
(
self):
"""fromRoman(toRoman(n))==n for all n"""
for
integer in
range(
1
, 4000
): ***
1
***
***
2
***
numeral =
roman.toRoman
(
integer)
result =
roman.fromRoman
(
numeral)
self.assertEqual
(
integer, result) ***
3
***
***1*** Nous avons déjà vu la fonction range, mais ici elle est appelée avec deux arguments, ce qui retourne une liste d'entiers commençant au premier argument (1) et comptant jusqu'au second argument (4000) non compris. L'intervalle retourné est donc 1..3999, ce qui est l'étendue des valeurs pouvant être converties en nombre romains valides.
***2*** Juste une note au passage, integer n'est pas un mot-clé de Python, ici c'est un nom de variable comme un autre.
***3*** La logique de test elle-même est très simple : on prend une valeur (integer), on la converti en chiffres romains (numeral), puis on converti ce nombre en chiffres romains en une valeur (result) qui doit être la même que celle de départ. Dans le cas contraire, assertEqual déclenche une exception et le test sera immédiatement considéré comme ayant échoué. Si tous les nombres correspondent, assertEqual s'exécutera silencieusement, la méthode testSanity entière s'achèvera silencieusement et le test sera considéré comme ayant passé.
Les deux dernières spécifications sont différentes des autres car elles semblent à la fois arbitraire et triviales :
- 7. toRoman doit toujours retourner des chiffres romains en majuscules.
- 8. fromRoman doit seulement accepter des chiffres romains en majuscules (il doit échouer s'il lui est passé une entrée en minuscules.
En fait, elles sont un peu arbitraire. Nous aurions pu stipuler, par exemple, que fromRoman accepterait une entrée en minuscules ou en casse mélangée. Mais elles ne sont pas totalement arbitraire pour autant, si toRoman retourne toujours une sortie en majuscule, fromRoman doit au moins accepter une entrée en majuscules, sinon notre test de cohérence (spécification n°6) échouera. Le fait qu'il accepte seulement des majuscules est arbitraire, mais comme tout intégrateur système vous le dira, la casse est toujours importante, mieux vaut donc spécifier le comportement face à la casse dès le début. Et si cela vaut la peine d'être spécifié, cela vaut la peine d'être testé.
Exemple 13.6. Tester la casse▲
class
CaseCheck
(
unittest.TestCase):
def
testToRomanCase
(
self):
"""toRoman should always return uppercase"""
for
integer in
range(
1
, 4000
):
numeral =
roman.toRoman
(
integer)
self.assertEqual
(
numeral, numeral.upper
(
)) ***
1
***
def
testFromRomanCase
(
self):
"""fromRoman should only accept uppercase input"""
for
integer in
range(
1
, 4000
):
numeral =
roman.toRoman
(
integer)
roman.fromRoman
(
numeral.upper
(
)) ***
2
***
***
3
***
self.assertRaises
(
roman.InvalidRomanNumeralError,
roman.fromRoman, numeral.lower
(
)) ***
4
***
***1*** Le plus intéressant dans ce cas de test, c'est toutes les choses qu'il ne teste pas. Il ne teste pas que la valeur retournée par toRoman est correcte ni même cohérente, ces questions sont traitées par d'autres cas de test. Nous avons un cas de test uniquement consacré à la casse. On pourrait être tenté de le combiner avec le test de cohérence, puisque ces deux tests parcourent toute l'étendue des valeurs et appellent toRoman.(7) Mais cela serait une violation de nos règles fondamentales : chaque cas de test doit répondre à une seule question. Imaginez que vous combiniez cette vérification de la casse avec le test de cohérence et que le test échoue. Vous auriez à faire une analyse en profondeur pour savoir quelle partie du cas de test en serait la cause. Si vous devez analyser les résultats de vos tests unitaires rien que pour savoir ce qu'ils signifient, il est certain que vous avez mal conçus vos cas de test.
***2*** Il y a ici une leçon similaire : même si «nous savons» que toRoman retourne toujours des majuscules, nous convertissons explicitement sa valeur de retour en majuscules pour tester que fromRoman accepte une entrée en majuscule. Pourquoi ? Parce que le fait que toRoman retourne toujours des majuscules est une spécification indépendante. Si nous changions cette spécification de manière, par exemple, à ce qu'il retourne toujours des minuscules, le cas de test testToRomanCase devrait être modifié, mais celui-ci passerait toujours. C'est une autre de nos règles fondamentales : chaque cas de test doit fonctionner de manière isolée de tous les autres. Chaque cas de test est un îlot.
***3*** Notez que nous n'assignons pas la valeur retournée par fromRoman. C'est syntaxiquement légal en Python, si une fonction retourne un valeur mais que l'appelant ne l'assigne pas, Python se contente de jeter cette valeur de retour. Dans le cas présent, c'est ce que nous voulons. Ce cas de test ne teste rien qui concerne la valeur de retour, il teste seulement que fromRoman accepte une entrée en majuscule sans déclencher d'exception.
***4*** Cette ligne est compliquée, mais elle est très similaire à ce que nous avons fait dans les test ToRomanBadInput et FromRomanBadInput. Nous testons que l'appel d'une fonction spécifique (roman.fromRoman) avec une fonction spécifique (numeral.lower(), la version en minuscules des chiffres romains en cours dans la boucle) déclenche une exception spécifique (roman.InvalidRomanNumeralError). Si c'est le cas (à chaque itération de la boucle) le test passe, s'il se passe quelque chose d'autre ne serait-ce qu'une fois (par exemple le déclenchement d'une autre exception ou le retour d'une valeur sans déclencher d'exception) le test échoue.
Dans le chapitre suivant, nous verrons comment écrire le code qui passera ces tests.