El desarrollo guiado por pruebas (TDD por sus siglas en inglés test-driven development) es una práctica de programación que consiste en crear primero los test que probaran la consistencia de los estados o de las transiciones de estados del objeto que se pretende desarrollar, para luego programar estas funcionalidades.
La idea es que estas pruebas sirvan como definición de requisitos, los cuales estaremos obligados a cumplir.
El procedimiento general para un TDD es el siguinete:
- Para cada estado o transición de estados, seguir estos pasos:
- Crear un test para un estado o para una transición de estado.
- Correr el test y esperar que falle.
- Generar el mínimo código que logre aprobar el test.
- Mejorar el código generado de manera que quede más limpio claro y preciso, procurando siempre mantener la funcionalidad.
TDD en Python
Veremos ahora como realizar un desarrollo guiado por pruebas en Python versión 2.7.2.
Usaremos de ejemplo el modelamiento de una persona. Diremos que una persona es simplemente un nombre, un apellido y una fecha de nacimiento.
Entonces lo primero que debemos hacer, es crear un test. Partiremos poniendo a prueba una nueva instancia de una persona, para esto creamos crearemos el fichero que contendrá todas las pruebas que realizaremos al objeto Persona y lo llamaremos “testPerson.py”. En el haremos nuestra primera prueba antes mencionada, para esto escribiremos lo siguiente:
import unittest
class TestPerson(unittest.TestCase):
def testCreate(self):
# Arrange
# Act
p = Person("Juan", "Perez", "1983-06-13")
# Assert
self.assertNotEqual(None, p)
Vamos viendo las partes que componen este código. Lo primero a lo que debemos poner atención es a la importación de la librería “unittest”, esta nos proveerá de una serie de herramientas que nos permitirá poner a prueba nuestros objetos, iremos viendo algunas de estas herramientas en la medida que vayamos generando nuestros distintos test.
Lo segundo a lo que hay que poner atención, es al hecho de que nuestra clase en la que haremos las pruebas, extiende de la clase “TestCase” de “unittest”. El nombre de nuestra clase no es relevante a la hora de evaluar nuestro objeto, pudiendo ser cualquier otro (la idea es que sea lo más descriptivo posible).
Lo último importante es el assert. Es finalmente en esta parte donde se evalúa si el objeto se comporta como se espera o si está fallando. En este caso hacemos un “self.assertNotEqual(None, p)”, lo que quiere decir es que la condición a evaluar es que el objeto “p” sea distinto de “None”, con lo que podremos determinar si creó correctamente la instancia de la clase Person o no.
Por un tema de orden, el código de dividió en tres partes: “Arrange”, “Act” y “Assert” que simbolizan preparar el ambiente de pruebas, ejecutar las acciones a probar y evaluar si se pasan o no las pruebas.
Para realizar las pruebas, debemos dirigirnos a la consola del sistema, acceder al directorio que contiene nuestro archivo “testperson.py” y ejecutar el siguiente comando:
python -m unittest discover -v
Que significa :
- python: ejecutar la aplicación de python
- -m unittest: con el módulo unittest para ejecutar las priebas
- discover: para que busque en el directorio todos los “TestCase”
- -v: de manera verbosa, para que nos muestre el detalle del procedimiento
Lo que nos entrega como resultado:
testCreate (testperson.TestPerson) ... ERROR
======================================================================
ERROR: testCreate (testperson.TestPerson)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/home/camello/Person/src/testperson.py", line 7, in testCreate
p = Person("Juan", "Perez", "1983-06-13")
NameError: global name 'Person' is not defined
----------------------------------------------------------------------
Ran 1 test in 0.001s
FAILED (errors=1)
Este error era de esperarse, puesto que nunca hemos creado la clase “Person”. Por lo tanto nuestro primer intento es crear la clase “Person” y volver a correr el test. Para eso creamos el fichero “person.py” con las siguientes líneas.
class Person(object):
pass
Importamos la clase “Person” en nuestra clase de pruebas:
from person import Person
import unittest
class TestPerson(unittest.TestCase):
...
Volvemos a correr el test:
testCreate (testperson.TestPerson) ... ERROR
======================================================================
ERROR: testCreate (testperson.TestPerson)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/home/camello/Person/src/testperson.py", line 8, in testCreate
p = Person("Juan", "Perez", "1983-06-13")
TypeError: object.__new__() takes no parameters
----------------------------------------------------------------------
Ran 1 test in 0.001s
FAILED (errors=1)
Ahora el error es distinto y tiene que ver con el hecho de que no existe un constructor para la clase “Person” que reciba esos tres parámetros. Por lo tanto la solución más simple podría ser crear el constructor que reciba dichos parámetros, pero que no haga nada.
class Person(object):
def __init__(self, name, latsName, birth):
pass
Corremos el test…
testCreate (testperson.TestPerson) ... ok ---------------------------------------------------------------------- Ran 1 test in 0.000s OK
Y ahora si lo pasamos sin problemas.
Hemos pasado el primer test, hora buscaremos un segundo estado a evaluar. Por ejemplo, recuperar el nombre de una persona recién creada. Un ejemplo de como hacer esto, sería agregar la prueba “testGetName” como sigue:
from person import Person
import unittest
class TestPerson(unittest.TestCase):
def testCreate(self):
# Arrange
# Act
p = Person("Juan", "Perez", "1983-06-13")
# Assert
self.assertNotEqual(None, p)
def testGetName(self):
# Arrange
p = Person("Juan", "Perez", "1983-06-13")
# Act
name = p.getName()
# Assert
self.assertEqual("Juan", name)
Corremos los test…
testCreate (testperson.TestPerson) ... ok
testGetName (testperson.TestPerson) ... ERROR
======================================================================
ERROR: testGetName (testperson.TestPerson)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/home/camello/Person/src/testperson.py", line 16, in testGetName
name = p.getName()
AttributeError: 'Person' object has no attribute 'getName'
----------------------------------------------------------------------
Ran 2 tests in 0.001s
FAILED (errors=1)
Vemos que testCreate sigue aprobando la evaluación sin problemas, pero testGetName falla por la simple razón de que el método “getName” no está definido en la clase “Person”. Por lo tanto, la solución más simple es crearlo:
class Person(object):
def __init__(self, name, latsName, birth):
pass
def getName(self):
pass
Corremos los test..
testCreate (testperson.TestPerson) ... ok
testGetName (testperson.TestPerson) ... FAIL
======================================================================
FAIL: testGetName (testperson.TestPerson)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/home/camello/Person/src/testperson.py", line 18, in testGetName
self.assertEqual("Juan", name)
AssertionError: 'Juan' != None
----------------------------------------------------------------------
Ran 2 tests in 0.001s
FAILED (failures=1)
Ahora “testGetName” falla porque “getName” no está retornando el valor esperado, es entonces cuando hacemos cambios en el código de manera que cumpla con requerido. Para esto guardaremos en una variable de la clase “Person” el nombre de la persona a la hora de crear el objeto y lo retornaremos cada vez que se llame al método “getName”.
class Person(object):
def __init__(self, name, latsName, birth):
self.name = name
def getName(self):
return self.name
Volvemos a correr los test…
testCreate (testperson.TestPerson) ... ok testGetName (testperson.TestPerson) ... ok ---------------------------------------------------------------------- Ran 2 tests in 0.000s OK
Si seguimos estos mismo pasos para la evaluación de la recuperación del apellido y la fecha de nacimiento de la persona, rápidamente obtendremos los siguientes resultados para nuestros test y para la clase “Person” respectivamente.
from person import Person
import unittest
class TestPerson(unittest.TestCase):
def testCreate(self):
...
def testGetName(self):
...
def testGetLastName(self):
# Arrange
p = Person("Juan", "Perez", "1983-06-13")
# Act
lastName = p.getLastName()
# Assert
self.assertEqual("Perez", lastName)
def testGetBirth(self):
# Arrange
p = Person("Juan", "Perez", "1983-06-13")
# Act
birth = p.getBirth()
# Assert
self.assertEqual("1983-06-13", birth)
class Person(object):
def __init__(self, name, lastName, birth):
self.name = name
self.lastName = lastName
self.birth = birth
def getName(self):
return self.name
def getLastName(self):
return self.lastName
def getBirth(self):
return self.birth
Obteniendo resultados satisfactorios en todos:
testCreate (testperson.TestPerson) ... ok testGetBirth (testperson.TestPerson) ... ok testGetLastName (testperson.TestPerson) ... ok testGetName (testperson.TestPerson) ... ok ---------------------------------------------------------------------- Ran 4 tests in 0.001s OK
Procediendo de la misma manera, podríamos agregar nuevas funcionalidades a las clase “Person”, como por ejemplo un método que retorne la edad de la persona, partiendo siempre con la creación del test que verifique la propiedad.