Testing en django: mejoras
A menudo, cuando me pongo a hacer algo en Django y escribo mis primeros tests, los noto pesados y desordenados. Me resulta difícil diferenciar entre tests unitarios, de integracción y de aceptación.
De la misma manera, suele ser una aventura añadir coverage, ya que nunca me acuerdo de cómo se hace.
En este artículo describiré cómo hacer ambas cosas.
Entorno
Lo primero es crear un entorno con lo que vamos a necesitar. Para ello crearemos un entorno virtual, lo que nos aislará un poco del resto. Para este artículo usaré python 2.7, aunque debería ser perfectamente compatible con python 3.
Creemos el entorno:
|
|
Ahora comencemos a usarlo:
|
|
Lo primero es instalar todo lo que nos hará falta. Para ello, creamos el archivo requirements.txt con el siguiente contenido:
|
|
Y ahora lo instalamos:
|
|
Así seguro que tendréis las mismas versiones que yo.
Proyecto
Vamos con un proyecto muy sencillo. Será más que eso: será mínimo:
|
|
De todo esto ya hablé en el artículo Creación de un sitio básico Django, así que por eso voy rápido.
A menudo necesito un par de funciones para transformar datetime
en timestamps
y viceversa, de manera que mis javascripts puedan comunicarse vía REST, así que éstas van a ser las funciones a probar:
|
|
Una vez tenemos todo esto… Comencemos.
Un poquito de organización
División en ficheros
¿A nadie más que a mí le molesta tener todos los tests en un único archivo tests.py?
Pues resulta que Django estś buscando todos los archivos que se llamen
test_*
, por lo que podemos crear el módulo “tests” (es decir, crear el
directorio “tests” con un archivo vacío "init.py") y se ejecutarán
todos los archivos cuyo nombre comience por test_
.
Sin embargo, esto me molesta también, ya que es información redundante. ¿No estamos ya dentro del módulo tests?
Dado que Django sólo toma como test cualquier clase que herede de TestCase
(ya sea de django.test
o de unittest
), es estúpido filtrar también por el nombre del archivo.
Por esa razón me gusta utilizar la opción -p"*.py"
, de manera que me busque los tests en todos los archivos python, y no tener que preocuparme del nombre:
|
|
unit/integration/acceptance
Otra cosa que me mosquea es tener que esperar mucho a la ejecución de los tests. Por eso me gusta tener unos tests de ejecución ultra rápida, los unitarios, que no necesitan la base de datos ni acceso a disco. De hecho, quiero asegurarme de que éstos no pueden acceder a la base de datos de ninguna manera, con el fin de evitar cualquier despiste.
Django no soporta nada de esto. De hecho, si alguna TestSuite
requiere una fixture, automáticamente generará una BBDD vacía e insertará los datos. No quiero eso.
La solución es implementar mi propio Runner
:
|
|
Como podéis ver, me creo el CustomizedRunner
que hereda de DiscoverRunner
. Así no me tengo que preocupar de reinventar la rueda y Django me ofrece todo lo que necesito. Lo único que me falta es un filtro para los tests.
Por eso creo tres Runners distintos: UnitRunner
, IntegrationRunner
y AcceptanceRunner
. Éstos, básicamente, filtran los tests que contengan cierta cadena en su nombre de módulo.
La única diferencia la pone el UnitRunner
, en el que me aseguro de que la base de datos no se toca. No es extrictamente necesario, pero no está de más.
Ahora puedo crearme la siguiente estructura:
|
|
Y podré situar los tests en el lugar adecuado.
Para seleccionar los tests que quiero ejecutar, basta con cambiar el runner:
|
|
Cobertura
Esta estructura de tests me permite algo más: puedo seleccionar la cobertura en función del tipo de test. Puede parecer una tontería, pero me interesa separar la cobertura de tests unitarios de la de los de integración, y la de aceptación no me interesa en absoluto.
Veamos primero cómo se ejecuta para los tests unitarios:
|
|
Un poco larga la línea, pero fácil de entender. Tan solo le indico dónde están los fuentes y que no me interesa la cobertura de los archivos de test ni de los propios de Django. Ahora muestro el resultado:
|
|
Fácil, ¿eh?
Aún más fácil
Yo soy muy vago y no voy a acordarme de una línea como ésa. Por eso he decidido meterlo todo en un Makefile:
|
|
Como veréis, he añadido algunos detalles para pasar el pep8 y pyflakes, y algunas etiquetas para saber qué está pasando y generar informes.
Los tests unitarios crearán el archivo de cobertura, los de integración añadirán sus datos y los de aceptación no generarán cobertura en absoluto.
De esta manera puedo ejecutar los tests de unit tantas veces como quiera, que serán muy rápidos.
Los tests
Por si alguien desea probar todo esto con datos reales, aquí están los tests:
|
|
Y aquí tenéis un ejemplo de ejecución:
|
|
Se puede afinar aún más, evitando entrar en algunos archivos que no nos interesan ( wsgi, settings), pero creo que esta parte es bastante sencilla comparada con todo lo anterior XD
Más información
En la documentación de Django podéis encontrar todo lo necesario.
Por otra parte, hace un tiempo que llevo pensando en hacer unas plantillas para hacer proyectos en Django y AngularJS, y hace poco que encontré un proyecto. Sin embargo, éste usaba versiones muy antiguas copiadas sobre el propio proyecto. Lo mejoré para que las resolviera como dependencias e hice el pull-request, pero aún no me han hecho ni caso.
Podéis encontrar este proyecto con el nombre de angularjs-django-rest-framework-seed. Ya le he añadido estas mejoras.
El proyecto original parece un poco abandonado :(