13.5. Prueba de fallo

No es suficiente probar que las funciones tienen éxito cuando se les pasa valores correctos; también debe probar que fallarán si se les da una entrada incorrecta. Y no sólo cualquier tipo de fallo; deben fallar de la manera esperada.

Recuerde los otros requisitos de toRoman:

  1. toRoman debería fallar cuando se le dé un entero fuera del rango 1 al 3999.
  2. toRoman debería fallar cuando se le dé un número no entero.

En Python las funciones indican los fallos lanzando excepciones y el módulo unittest proporciona métodos para probar si una función lanza una excepción en particular cuando se le da una entrada incorrecta.

Ejemplo 13.3. Prueba de toRoman con entrada incorrecta


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 clase TestCase de unittest proporciona el método assertRaises, que toma los siguientes argumentos: la excepción que esperamos, la función que vamos a probar, y los argumentos que le queremos pasar a esa función (si la función que vamos a probar toma más de un argumento, se los pasamos todos a assertRaises, en orden, y se los pasará directamente a la función que queremos probar). Preste mucha atención a lo que estamos haciendo aquí: en lugar de llamar directamente a toRoman y comprobar manualmente que lanza una excepción en particular (encerrándolo en un bloque try...except, assertRaises lo encapsula por nosotros. Todo lo que hacemos es darle la excepción (roman.OutOfRangeError), la función (toRoman) y los argumentos de toRoman (4000), y assertRaises se encarga de invocar a toRoman y comprueba para asegurarse de que lanza roman.OutOfRangeError. (Observe también que está pasando la función toRoman en sí como un argumento; no la está invocando y no está pasando su nombre como una cadena. ¿Le he mencionado últimamente lo útil que es que todo en Python sea un objeto, incluyendo funciones y excepciones?)
2 Además de las pruebas de números que son demasiado grandes, necesita probar los números que son demasiado pequeños. Recuerde, los números romanos no pueden expresar 0 o números negativos, así que tenemos un caso de prueba para cada uno (testZero y testNegative). En testZero estamos probando que toRoman lance una excepción roman.OutOfRangeError cuando se le pasa 0; si no lanza una roman.OutOfRangeError (bien porque devuelva un valor, o porque lanza otra excepción), se considerará que ha fallado esta prueba.
3 El tercer requisito especifica que toRoman no puede aceptar un número que no sea entero, así que aquí probamos para aseguranos de que toRoman lanza una excepción roman.NotIntegerError cuando se le llama con 0.5. Si toRoman no lanza una roman.NotIntegerError se considerará que ha fallado esta prueba.

Los dos siguientes requisitos son similares a los tres primeros, excepto que se aplican a fromRoman en lugar de a toRoman:

  1. fromRoman debería tomar un número romano válido y devolver el número que representa.
  2. fromRoman debería fallar cuando se le dé un número romano no válido.

El 4º requisito se trata de la misma manera que el requisito 1, iterando sobre una muestra de valores conocidos y probándolos por turno. El 5º requisito se trata de la misma manera que el 2º y el 3º, probando una serie de entradas incorrectas y asegurándose de que fromRoman lanza la excepción adecuada.

Ejemplo 13.4. Prueba de entradas incorrectas a 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 No hay mucho nuevo que decir aquí; el patrón es exactamente el mismo que usamos para probar la entrada incorrecta en toRoman. Señalaré brevemente que tenemos otra excepción: roman.InvalidRomanNumeralError. Esto hace un total de tras excepciones propias que necesitaremos definir en roman.py (junto con roman.OutOfRangeError y roman.NotIntegerError). Veremos cómo definir estas excepciones cuando empecemos a escribir roman.py, más adelante en este capítulo.