13.5. Tester l’échec

Il ne suffit pas de tester que nos fonctions réussissent lorsqu’on leur passe des entrées correctes, nous devons aussi tester qu’elles échouent lorsque les entrées sont incorrectes. Et pas seulement qu’elles échouent, qu’elles échouent de la manière prévue.

Rappelez-vous nos autres spécifications pour toRoman :

  1. toRoman doit échouer s’il lui est passé un entier hors de l’intervalle 1 à 3999.
  2. 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 :

  1. fromRoman doit prendre un nombre en chiffres romains valide et retourner la valeur qu’il représente.
  2. 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.