Capítulo 15. Refactorización

15.1. Gestión de fallos

A pesar de nuestros mejores esfuerzos para escribir pruebas unitarias exhaustivas, los fallos aparecen. ¿A qué me refiero con “un fallo”? Un fallo es un caso de prueba que aún no hemos escrito.

Ejemplo 15.1. El fallo

>>> import roman5
>>> roman5.fromRoman("") 1
0
1 ¿Recuerda cuando en la sección anterior vimos que una cadena vacía coincidía con la expresión regular que estábamos usando para los números romanos válidos? Bien, resulta que esto sigue siendo cierto para la versión final de la expresión regular. Y esto es un fallo; queremos que una cadena vacía lance una excepción InvalidRomanNumeralError igual que cualquier otra secuencia de caracteres que no represente un número romano válido.

Tras reproducir el fallo, y antes de arreglarlo, debería escribir un caso de prueba que falle que ilustre el fallo.

Ejemplo 15.2. Prueba del fallo (romantest61.py)


class FromRomanBadInput(unittest.TestCase):                                      

    # se omiten los casos de prueba anteriores por claridad (no han
    # cambiado)

    def testBlank(self):
        """fromRoman should fail with blank string"""
        self.assertRaises(roman.InvalidRomanNumeralError, roman.fromRoman, "") 1
1 Cosas muy simples aquí. Invocamos fromRoman con una cadena vacía y nos aseguramos de que lanza una excepción InvalidRomanNumeralError. La parte dura fue encontrar el fallo; ahora que lo sabemos, probarlo es la parte sencilla.

Como el código tiene un fallo, y ahora tenemos un caso de prueba que busca ese fallo, la prueba fallará:

Ejemplo 15.3. Salida de romantest61.py frente a roman61.py

fromRoman should only accept uppercase input ... ok
toRoman should always return uppercase ... ok
fromRoman should fail with blank string ... FAIL
fromRoman should fail with malformed antecedents ... ok
fromRoman should fail with repeated pairs of numerals ... ok
fromRoman should fail with too many repeated numerals ... ok
fromRoman should give known result with known input ... ok
toRoman should give known result with known input ... ok
fromRoman(toRoman(n))==n for all n ... ok
toRoman should fail with non-integer input ... ok
toRoman should fail with negative input ... ok
toRoman should fail with large input ... ok
toRoman should fail with 0 input ... ok

======================================================================
FAIL: fromRoman should fail with blank string
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\docbook\dip\py\roman\stage6\romantest61.py", line 137, in testBlank
    self.assertRaises(roman61.InvalidRomanNumeralError, roman61.fromRoman, "")
  File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
    raise self.failureException, excName
AssertionError: InvalidRomanNumeralError
----------------------------------------------------------------------
Ran 13 tests in 2.864s

FAILED (failures=1)

Ahora podemos arreglar el fallo.

Ejemplo 15.4. Arreglo del fallo (roman62.py)

Este fichero está disponible en py/roman/stage6/ dentro del directorio de ejemplos.


def fromRoman(s):
    """convert Roman numeral to integer"""
    if not s: 1
        raise InvalidRomanNumeralError, 'Input can not be blank'
    if not re.search(romanNumeralPattern, s):
        raise InvalidRomanNumeralError, 'Invalid Roman numeral: %s' % s

    result = 0
    index = 0
    for numeral, integer in romanNumeralMap:
        while s[index:index+len(numeral)] == numeral:
            result += integer
            index += len(numeral)
    return result
1 Sólo se necesitan dos líneas de código: una comprobación explícita en busca de una cadena vacía, y una sentencia raise.

Ejemplo 15.5. Salida de romantest62.py frente a roman62.py

fromRoman should only accept uppercase input ... ok
toRoman should always return uppercase ... ok
fromRoman should fail with blank string ... ok 1
fromRoman should fail with malformed antecedents ... ok
fromRoman should fail with repeated pairs of numerals ... ok
fromRoman should fail with too many repeated numerals ... ok
fromRoman should give known result with known input ... ok
toRoman should give known result with known input ... ok
fromRoman(toRoman(n))==n for all n ... ok
toRoman should fail with non-integer input ... ok
toRoman should fail with negative input ... ok
toRoman should fail with large input ... ok
toRoman should fail with 0 input ... ok

----------------------------------------------------------------------
Ran 13 tests in 2.834s

OK 2
1 El caso de prueba de la cadena vacía pasa ahora, así que se ha corregido el fallo.
2 Todos los demás casos de prueba siguen pasando, lo que significa que este arreglo no ha roto nada más. Deje de programar.

Programar de esta manera no hace más fácil arreglar fallos. Los fallos simples (como éste) precisan casos de prueba sencillos; los fallos complejos precisan casos de prueba complejos. En un entorno centrado en las pruebas puede parecer que se tarda más en corregir un fallo, ya que se necesita articular en el código exactamente cual es el fallo (escribir el caso de prueba) y luego corregir el fallo en sí. Si el caso de prueba no tiene éxito ahora, tendremos que averiguar si el arreglo fue incorrecto o si el propio caso de prueba tiene un fallo. Sin embargo, a la larga, este tira y afloja entre código de prueba y código probado se amortiza por sí solo. Además, ya que podemos ejecutar de nuevo de forma sencilla todos los casos de prueba junto con el código nuevo, es mucho menos probable que rompamos código viejo al arreglar el nuevo. La prueba unitaria de hoy es la prueba de regresión de mañana.