Capítulo 14. Programación Test-First[17]

14.1. roman.py, fase 1

Ahora que están terminadas las pruebas unitarias, es hora de empezar a escribir el código que intentan probar esos casos de prueba. Vamos a hacer esto por etapas, para que pueda ver fallar todas las pruebas, y verlas luego pasar una por una según llene los huecos de roman.py.

Ejemplo 14.1. roman1.py

Este fichero está disponible en py/roman/stage1/ en el directorio de ejemplos.

Si aún no lo ha hecho, puede descargar éste ejemplo y otros usados en este libro.

"""Convert to and from Roman numerals"""

#Define exceptions
class RomanError(Exception): pass                1
class OutOfRangeError(RomanError): pass          2
class NotIntegerError(RomanError): pass
class InvalidRomanNumeralError(RomanError): pass 3

def toRoman(n):
    """convert integer to Roman numeral"""
    pass                                         4

def fromRoman(s):
    """convert Roman numeral to integer"""
    pass
1 Así es como definimos nuestras excepciones propias en Python. Las excepciones son clases, y podemos crear las nuestras propias derivando las excepciones existentes. Se recomienda encarecidamente (pero no se precisa) que derive Exception, que es la clase base de la que heredan todas las excepciones que incorpora Python. Aquí defino RomanError (que hereda de Exception) para que actúe de clase base para todas las otras excepciones que seguirán. Esto es cuestión de estilo; podría igualmente haber hecho que cada excepción derivase directamente de la clase Exception.
2 Las excepciones OutOfRangeError y NotIntegerError se usarán en toRoman para indicar varias formas de entrada inválida, como se especificó en ToRomanBadInput.
3 La excepción InvalidRomanNumeralError se usará en fromRoman para indicar entrada inválida, como se especificó en FromRomanBadInput.
4 En esta etapa, queremos definir la API de cada una de nuestras funciones, pero no programarlas aún, así que las dejamos vacías usando la palabra reservada de Python pass.

Ahora el momento que todos esperan (redoble de tambor, por favor): finalmente vamos a ejecutar la prueba unitaria sobre este pequeño módulo vacío. En este momento deberían fallar todas las pruebas. De hecho, si alguna prueba pasa en la fase 1, deberíamos volver a romantest.py y evaluar por qué programamos una prueba tan inútil que pasa en funciones que no hacen nada.

Ejecute romantest1.py con la opción -v, que le dará una salida más prolija para que pueda ver exactamente qué sucede mientras se ejecuta cada caso. Con algo de suerte, su salida debería ser como ésta:

Ejemplo 14.2. Salida de romantest1.py frente a roman1.py

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

======================================================================
ERROR: fromRoman should only accept uppercase input
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 154, in testFromRomanCase
    roman1.fromRoman(numeral.upper())
AttributeError: 'None' object has no attribute 'upper'
======================================================================
ERROR: toRoman should always return uppercase
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 148, in testToRomanCase
    self.assertEqual(numeral, numeral.upper())
AttributeError: 'None' object has no attribute 'upper'
======================================================================
FAIL: fromRoman should fail with malformed antecedents
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 133, in testMalformedAntecedent
    self.assertRaises(roman1.InvalidRomanNumeralError, roman1.fromRoman, s)
  File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
    raise self.failureException, excName
AssertionError: InvalidRomanNumeralError
======================================================================
FAIL: fromRoman should fail with repeated pairs of numerals
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 127, in testRepeatedPairs
    self.assertRaises(roman1.InvalidRomanNumeralError, roman1.fromRoman, s)
  File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
    raise self.failureException, excName
AssertionError: InvalidRomanNumeralError
======================================================================
FAIL: fromRoman should fail with too many repeated numerals
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 122, in testTooManyRepeatedNumerals
    self.assertRaises(roman1.InvalidRomanNumeralError, roman1.fromRoman, s)
  File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
    raise self.failureException, excName
AssertionError: InvalidRomanNumeralError
======================================================================
FAIL: fromRoman should give known result with known input
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 99, in testFromRomanKnownValues
    self.assertEqual(integer, result)
  File "c:\python21\lib\unittest.py", line 273, in failUnlessEqual
    raise self.failureException, (msg or '%s != %s' % (first, second))
AssertionError: 1 != None
======================================================================
FAIL: toRoman should give known result with known input
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 93, in testToRomanKnownValues
    self.assertEqual(numeral, result)
  File "c:\python21\lib\unittest.py", line 273, in failUnlessEqual
    raise self.failureException, (msg or '%s != %s' % (first, second))
AssertionError: I != None
======================================================================
FAIL: fromRoman(toRoman(n))==n for all n
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 141, in testSanity
    self.assertEqual(integer, result)
  File "c:\python21\lib\unittest.py", line 273, in failUnlessEqual
    raise self.failureException, (msg or '%s != %s' % (first, second))
AssertionError: 1 != None
======================================================================
FAIL: toRoman should fail with non-integer input
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 116, in testNonInteger
    self.assertRaises(roman1.NotIntegerError, roman1.toRoman, 0.5)
  File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
    raise self.failureException, excName
AssertionError: NotIntegerError
======================================================================
FAIL: toRoman should fail with negative input
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 112, in testNegative
    self.assertRaises(roman1.OutOfRangeError, roman1.toRoman, -1)
  File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
    raise self.failureException, excName
AssertionError: OutOfRangeError
======================================================================
FAIL: toRoman should fail with large input
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 104, in testTooLarge
    self.assertRaises(roman1.OutOfRangeError, roman1.toRoman, 4000)
  File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
    raise self.failureException, excName
AssertionError: OutOfRangeError
======================================================================
FAIL: toRoman should fail with 0 input                                 1
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 108, in testZero
    self.assertRaises(roman1.OutOfRangeError, roman1.toRoman, 0)
  File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
    raise self.failureException, excName
AssertionError: OutOfRangeError                                        2
----------------------------------------------------------------------
Ran 12 tests in 0.040s                                                 3

FAILED (failures=10, errors=2)                                         4
1 La ejecución del script lanza unittest.main(), que a su vez llama a cada caso de prueba, que es lo mismo que decir cada método definido dentro de romantest.py. Por cada caso de uso, imprime la cadena de documentación del método y si ha pasado o fallado la prueba. Como esperábamos, no ha pasado ninguna de ellas.
2 Por cada prueba fallida, unittest imprime la información de traza que muestra qué sucedió. En este caso, la llamada a assertRaises (llamada también failUnlessRaises) lanzó una AssertionError porque esperaba que toRoman lanzase una OutOfRangeError y no lo hizo.
3 Tras los detalles, unittest muestra un resumen de cuántas pruebas ha realizado y cuánto tiempo le ha tomado.
4 En general, la prueba unitaria ha fallado porque no ha pasado al menos uno de los casos de prueba. Cuando un caso de prueba no pasa, unittest distingue entre fallos y errores. Un fallo es una llamada a un método assertXYZ, como assertEqual o assertRaises, que falla porque la condición asertada no es cierta o no se lanzó la excepción esperada. Un error es cualquier otro tipo de excepción lanzada en el código que está probando o del propio caso unitario de prueba. Por ejemplo, el método testFromRomanCase (“fromRoman sólo debería aceptar entrada en mayúsculas”) dejó un error, porque la llamada a numeral.upper() lanzó una excepción AttributeError, ya que se suponía que toRoman debería devolver una cadena pero no lo hizo. Pero testZero (“toRoman debería fallar con una entrada 0”) dejó un fallo, ya que la llamada a fromRoman no lanzó la excepción InvalidRomanNumeral que estaba esperando assertRaises.

Footnotes

[17] El nombre de esta técnica se toma de la Programación Extrema (o XP)