Testing en Python

Imagen de resetreboot

¿Que son los tests?

Los tests son funciones y código específicamente diseñado para probar funcionalidad de nuestro código y asegurarnos, en la medida de lo posible, que funcionan.

Especialmente interesante es su capacidad de detectar cuando, al hacer cambios, rompemos cosas que funcionan antes incluso de subir a nuestro master de nuestro gestor de versiones o entornos de prueba. A pesar de lo maravillosos que son, normalmente en el MundoReal™ los plazos no dan para hacer buenos tests (si llega para hacer tests, claro) o bien nos da pereza (total, nuestro código es estupendo, ¿verdad?)

Teoría básica de los tests

El testing consiste en, a grandes rasgos, crear situaciones hipotéticas, hacer pasar a nuestro código por ellas y comprobar que:

  1. No casca
  2. Se comporta como esperamos que lo haga

Supongamos que tenemos una función def suma(a, b): que suma a y b. Un test para esta función, llamaría a la función con los parámetros a=2 y b=2 y comprobaría que el resultado es 4, para, a continuación, probar que con a=2 y b=3 el resultado es 5 e incluso que vale lo mismo si a=3 y b=2.

Como se puede comprobar, hacer tests es un proceso de razonamiento por parte del programador, que requiere de buscarle las cosquillas a nuestro código, para que no se nos pase ningún caso. Sin embargo, la realidad es que los tests, aunque empiecen con buena cobertura, se van mejorando según los bugs asoman su fea cabeza, los aplastamos y añadimos el caso a nuestros tests para que no se vuelva a repetir.

Conceptos de testing

Un poco de terminología para entender este mundillo:

  • Fixture
    Es la preparación del entorno necesaria para realizar un test, ya sea carga de una base de datos de pruebas, lanzamiento de servicios externos o situar los componentes en determinado estado. Normalmente se prepara para uno o varios tests a la vez.
  • Test Case
    Es un test, un caso. Una de las unidades que prueban algo.
  • Suite de tests
    Es un conjunto de tests. Normalmente, irá pareado con los módulos de tu programa.
  • Runner
    Es la parte encargada de orquestar y lanzar los tests. Normalmente usaremos un framework o extenderemos uno.

Testing en Python

Ahora, tras la teoría, vamos al meollo de la cuestión.

Python nos provee de una librería llamada, en un alarde de originalidad, unittest que es el que usaremos para escribir nuestros tests. Pesos pesados como Django, extienden esta librería para sus frameworks de tests, con lo que nos conviene saber cómo funciona.

Imaginemos que vamos a testear nuestra librería de funciones matemáticas "mates", así que creamos un fichero que llamaremos como queramos, en este caso "mates_testing.py" y lo añadiremos como otro módulo python en nuestro código.

import mates
import unittest
 
class TestMates(unittest.TestCase):
    def setUp(self):
        mates.initTrigonometry()
 
    def test_suma(self):
        self.assertEqual(suma(2, 2), 4)
        self.assertEqual(suma(2, 3), 5)
        self.assertEqual(suma(2, 3), suma(2, 3))

La clase que hemos creado, engloba varios tests (aunque en este caso. sólo tengamos uno), hereda de unittest.TestCase y lo que hacemos es crear métodos que empiezan con test en el nombre. Esto es importante, ya que es cómo el código de testeo detectará lo que son tests y los ejecutará, ignorando el resto. Esto permite tener dentro de nuestra clase métodos que llamaremos nosotros cuando necesitemos, y que no sean ejecutados como tests.

El método setUp es lo primero que se ejecutará siempre, antes de ir lanzando los tests uno tras otro, es donde prepararemos los fixtures y lanzaremos la inicializaciones de nuestro código. Aquí podemos crear conexiones a la base de datos, crear nuevas bases de datos, generar cosas, o, como en este caso, inicializar algo que tiene que ver con la trigonometría.

Assert

Los asserts son la parte más importante de nuestro código de test. Estas funciones permiten comprobar que una condición determinada se cumple. En nuestro ejemplo, self.assertEqual compara que ambos parámetros son iguales, y en caso de no serlo, avisa al runner para que marque el fallo y lo notifique.

Tipos de assert

  • assertEqual(a, b): Comprueba que a == b
  • assertNotEqual(a, b): Comprueba que a != b
  • assertTrue(x): Comprueba que x evalúa a True
  • assertFalse(x): Comprueba que x evalúa a False
  • assertIs(a, b): Comprueba "a is b" (que a y b evalúan al mismo objeto)
  • assertIsNot(a, b): Comprueba que a y b no evalúan al mismo objeto
  • assertIsNone(x): Comprueba que x es None
  • assertIsNotNone(x): Comprueba que x NO es None
  • assertIn(a, b): Comprueba que a está en b
  • assertNotIn(a, b): Comprueba que a no está en b
  • assertIsInstance(a, b): Comprueba que a es instancia de b (con isinstance)
  • assertNotIsInstance(a, b): Comprueba que a no es instancia de b (con isinstance)

También existen para comprobar que una función con ciertos parámetros lanza un tipo de Exception determinado, pero dejamos eso para revisar en la documentación oficial.

Por último, aunque en el ejemplo no lo hemos puesto, existe el método tearDown() que se ejecutará al finalizar los tests y sólo si setUp no ha dado problemas. Sirve para hacer limpieza tras los tests, eliminando bases de datos de prueba o liberando recursos.

Ejecutar tests

Para lanzar nuestros tests, vamos a instalar el framework "nose". Si estáis usando virtualenv (y si no, ¿a qué estáis esperando?) podéis instalarlo con pip install nose. Si no, podéis instalarlo con vuestro gestor de paquetes favorito.

Nose se va a encargar de localizar, ejecutar, recopilar resultados y darnos una salida bonita y clara de la ejecución de todos nuestros tests.

Una vez instalado, ejecutando nosetest en la consola, en nuestro directorio de proyecto o código, él hará todo el trabajo sucio. Si todo va bien y nuestro código (y los tests) son buenos, tendremos una salida tal que así:

..................................
----------------------------------------------------------------------
Ran 34 tests in 1.440s
 
OK

que viene a decir que todo ha salido estupendamente. Si algún test falla, lo marcará con una F y dirá qué assert es el que ha fallado. Si tenemos una E, es que ha saltado alguna excepción sin controlar y nos pondrá el traceback por pantalla. Finalmente, un resumen con todos los fallos y errores encontrados.

Con esto, tras hacer nuestros cambios al código, podremos ver de un vistazo rápido, que no hemos roto otra cosa aparte.

Nose tiene muchas ventajas como runner de nuestros tests. Aparte de hacer la vida más sencilla a la hora de encontrar y ejecutar todos nuestros tests aunque seamos muy desordenados, tiene un buen montón de plugins de terceros que nos pueden ayudar a que la tarea de testear nuestro código, sea más llevadera.

Despedida y cierre

En resumen, de esto trata todo esto: De ser paranoicos e hijoputas con nuestro código en los tests, buscándole las cosquillas para ver dónde puede fallar, para que cuando salga al salvaje MundoReal™ tenga mayores posibilidades de sobrevivir sin que pete. Si tenéis dudas o algo que añadir, los comentarios son siempre bienvenidos

Happy testing!

5 comments

4
Mar

Imagen de elautoestopista

Pues me ha aclarado muchas cosas, que me estoy iniciando tanto en la programación en serio como en Python.
¿Por qué no aparece en portada?

Lista de Correo #BOFHers - http://www.freelists.org/list/bofhers
Síguenos también el Twitter, en el hastag #BOFHers (y derivados)
¡También tenemos un servidor IRC! irc.bofhers.com:6667 canal #BOFHers

5
Mar

Imagen de resetreboot

Pues creo que no aparece en portada por que en vez de Artículo, escogí "Tips". No se si hay alguna forma de subirlo.

Y me alegro de que te haya resultado útil.

7
Mar

Imagen de vfmBOFH

Hay un ticker en la página de edición, sección "Opciones de publicación" que pone bien clarito "Promovido a portada".

Si haces una entrada de blog va a portada directo, creo. Pero si son páginas de libro o así, hay que promoverlo.

Atentamente, La voz ésa del interior de tu cabeza que oyes cuando lees algo.

17
Mar

Imagen de resetreboot

Ahhh, míralo tú... Pues nada, despistes de uno. Tengo que postear más a menudo.

8
Abr

Imagen de PhobosDBM

Puntualmente, cuando el tiempo lo permite, hago algunas pruebas con JUnit (lo mismo que ha explicado resetreboot, pero para Java), aunque también he de decir que como las manos de un consultor psicópata con mala leche y ganas de petarte Redmine no hay nada.

Pero supongo que dependerá de cada empresa. En mi caso, prima más la prisa por sacar cosas sin terminar que ceder tiempo para hacer las cosas debidamente.