1.6. Pruebas Python

1.6.1. Teoría

Se trata de hacer pasar al código por un conjunto de pruebas que comprueben que todo funciona como se espera. Programar tests permite:

  • Controlar la calidad del programa.
  • Saber si una modificación ha roto otra parte del código.
  • En cierta forma documentan el código, ya que pueden tomarse como ejemplos del funcionamiento.
  • Tener objetivos claros.

Aunque no todo es perfecto:

  • Cuesta tiempo escribirlos.
  • Hay que modificarlos si se modifica el código.
  • No garantizan la perfección del programa.

Cada prueba debe funcionar cuando las entradas son correctas, y debe fallar o lanzar una excepción si los valores son incorrectos. Además tienen que:

  • Ejecutarse sin interacción humana.
  • Saber si falla algo sin interacción humana.
  • Ser independientes.

Cobertura

El objetivo de los test es detectar todos los errores. Lo mínimo que se ha de cumplir es que todo el código sea ejecutado por lo menos una vez.

Integración

Los test deben pasarse cada vez que se cambie el código.

Mock

A veces no podemos llamar a ciertos objetos la ligera para probarlos, porque modificarían la base de datos o producirían alguna operación que no queremos llevar a cabo en las pruebas.

Mock sirve para simular el comportamiento de objetos.

1.6.2. In situ con doctest

El módulo doctest busca partes de la documentación que parezcan sesiones interactivas, y las ejecuta para verificar que funcionan exactamente como están escritas. Hay varias motivos para usar doctest:

  • Verificar que los docstrings están al día, ya que los ejemplos siguen funcionando.
  • Realizar pruebas de regresión.
  • Para escribir tutorioales en la propia documentación.

A continuación un pequeño pero completo ejemplo:

"""
This is the "example" module.

The example module supplies one function, factorial().  For example,

>>> factorial(5)
120
"""

def factorial(n):
    """Return the factorial of n, an exact integer >= 0.

    If the result is small enough to fit in an int, return an int.
    Else return a long.

    >>> [factorial(n) for n in range(6)]
    [1, 1, 2, 6, 24, 120]
    >>> [factorial(long(n)) for n in range(6)]
    [1, 1, 2, 6, 24, 120]
    >>> factorial(30)
    265252859812191058636308480000000L
    >>> factorial(30L)
    265252859812191058636308480000000L
    >>> factorial(-1)
    Traceback (most recent call last):
        ...
    ValueError: n must be >= 0

    Factorials of floats are OK, but the float must be an exact integer:
    >>> factorial(30.1)
    Traceback (most recent call last):
        ...
    ValueError: n must be exact integer
    >>> factorial(30.0)
    265252859812191058636308480000000L

    It must also not be ridiculously large:
    >>> factorial(1e100)
    Traceback (most recent call last):
        ...
    OverflowError: n too large
    """

    import math
    if not n >= 0:
        raise ValueError("n must be >= 0")
    if math.floor(n) != n:
        raise ValueError("n must be exact integer")
    if n+1 == n:  # catch a value like 1e300
        raise OverflowError("n too large")
    result = 1
    factor = 2
    while factor <= n:
        result *= factor
        factor += 1
    return result


if __name__ == "__main__":
    import doctest
    doctest.testmod()

Si ejecuta example.py directamente desde la línea de comandos, doctest hace su magia:

$ python example.py
$

No dice nada! Eso es normal, significa que todos los ejemplor funcionaron. Pase el parámetro -v al script, para ver lo que ha hecho:

$ python example.py -v
Trying:
    factorial(5)
Expecting:
    120
ok
Trying:
    [factorial(n) for n in range(6)]
Expecting:
    [1, 1, 2, 6, 24, 120]
ok
Trying:
    [factorial(long(n)) for n in range(6)]
Expecting:
    [1, 1, 2, 6, 24, 120]
ok

Etcétera, acabando así:

Trying:
    factorial(1e100)
Expecting:
    Traceback (most recent call last):
        ...
    OverflowError: n too large
ok
2 items passed all tests:
   1 tests in __main__
   8 tests in __main__.factorial
9 tests in 2 items.
9 passed and 0 failed.
Test passed.
$

Esto es lo que necesita saber para empezar a usar doctest!

Nota

Usar este tipo de pruebas ensucia mucho el código. La solución está en sacarlo a un archivo diferente, para que el código se quede más limpio:

1
2
3
4
5
6
7
import doctest

def factorial(n):
    return 1 if n < 1 else n * factorial(n-1)

if __name__ == "__main__":
    doctest.testfile('example2.txt')

1.6.3. Baterías con unittest

Hay muchas ocasiones en las que las pruebas realizadas con doctest se nos quedarán cortas. Unittest nos ofrece toda la potencia del lenguaje para probar nuestros programas.

Conceptos que facilita:

  • test fixture: La preparación necesaria para ejecutar uno o varios test, y la correspondiente limpieza.
  • test case: La más simple unidad de testeo. Prueba la respuesta en determinadas condiciones. La clase TestCase sirve de base.
  • test suite: Una coleccion de casos, suites, o ambos. Se usa para agrupar pruebas.
  • test runner: Sirve para organizar la ejecución de los test y cómo representar su salida.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import unittest

def factorial(n):
    return 1 if n < 1 else n * factorial(n-1)

class tester (unittest.TestCase):
    def test_1(self):
        self.assertEqual(1, factorial(1))

if __name__ == "__main__":
    unittest.main()

Dicotomía de un test

Los tests suelen dividirse en 3 pasos:

  1. Inicialización: preparar lo que se necesita para el test.
  2. Ejecución: que es la línea en la que se llama a la funcionalidad a probar.
  3. Comprobación: que es el test propiamente dicho, expresado en forma de verificaciones.

Funciones especiales

Hay algunas funciones especiales, que son:

  • setUp, que permite inicializar la fixture, es decir, establecer un entorno de pruebas para cada prueba. Se ejecutará siempre antes de cada test.
  • tearDown, que realiza la operación contraria, es decir, se ejecuta siempre después de cada test.
  • setUpClass, que se ejecuta una única vez al comienzo de la clase.
  • tearDownClass, que se lanza cuando se han terminado todos los tests.

Comprobaciones

Se puede comprobar si el código lanza una excepción.

Método Chequea Versión
assertRaises(exc, fun, *args, **kwds) fun(*args, **kwds) lanza exc  
assertRaisesRegexp(exc, re, fun, *args, **kwds) fun(*args, **kwds) lanza exc y coincide con re 2.7

Se puede comprobar si el resultado es el valor esperado o menor, mayor, contiene, etc. Los siguientes métodos aceptan un parámetro msg para especificar el mensaje de error.

Método Chequea Versión
assertEqual(a, b) a == b  
assertNotEqual(a, b) a != b  
assertTrue(x) bool(x) is True  
assertFalse(x) bool(x) is False  
assertIs(a, b) a is b 2.7
assertIsNot(a, b) a is not b 2.7
assertIsNone(x) x is None 2.7
assertIsNotNone(x) x is not None 2.7
assertIn(a, b) a in b 2.7
assertNotIn(a, b) a not in b 2.7
assertIsInstance(a, b) isinstance(a, b) 2.7
assertNotIsInstance(a, b) not isinstance(a, b) 2.7

Ver también

  • nose es un paquete para facilitar la ejecución de pruebas
  • hamcrest es un paquete para la comprobación de condiciones.
  • minimock es un paquete .

1.6.4. TDD

Desarrollo conducido por pruebas.

Bibliografía