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.
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):
numeral = roman.toRoman(integer)
result = roman.fromRoman(numeral)
self.assertEqual(integer, result)
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.
Juste une note au passage, integer n’est
pas un mot-clé de Python, ici c’est un
nom de variable comme un autre.
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 :
toRoman doit toujours retourner des chiffres romains en majuscules.
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()) 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())
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.
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.
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.
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.
Footnotes
[7] «Je peux résister à tout, sauf à la
tentation.» Oscar Wilde