Link Search Menu Expand Document

Testing en Python

Dentro de la ingenier铆a software y la programaci贸n en general, el testing es una de las partes m谩s importantes que nos encontraremos en casi cualquier proyecto. De hecho es com煤n dedicar m谩s tiempo a probar que el c贸digo funciona correctamente que a escribirlo. A continuaci贸n veremos las formas m谩s comunes de testear el c贸digo en Python, desde lo m谩s b谩sico a conceptos algo m谩s avanzados.

Tests Manuales y Tests Automatizados

Aunque sea la primera vez que leas acerca de testing en Python, estoy seguro de que ya has ejecutado tests sobre tu c贸digo sin darte cuenta. De acuerdo a su forma de ejecuci贸n, los podemos clasificar en:

  • Tests manuales: Son tests ejecutados manualmente por una persona, probando diferentes combinaciones y viendo que el comportamiento del c贸digo es el esperado. Sin duda los has realizado alguna vez.
  • Tests autom谩ticos: Se trata de c贸digo que testea que otro c贸digo se comporta correctamente. La ejecuci贸n es autom谩tica, y permite ejecutar gran cantidad de verificaciones en muy poco tiempo. Es la forma m谩s com煤n, pero no siempre es posible automatizar todo.

Imaginemos que hemos escrito una funci贸n que calcula la media de los valores que se pasan en una lista como entrada.

def calcula_media(*args):
    return(sum(*args)/len(*args))

A nadie se le ocurrir铆a publicar nuestra funci贸n calcula_media sin haber hecho alguna verificaci贸n anteriormente. Podemos por ejemplo probar con los siguientes datos y ver si la funci贸n hace lo que se espera de ella. Al hacer esto ya estar铆amos probando manualmente nuestro c贸digo.

print(calcula_media([3, 7, 5]))
# 5.0

print(calcula_media([30, 0]))
# 15.0

Con bases de c贸digo peque帽as y donde s贸lo trabajemos nosotros, tal vez sea suficiente, pero a medida que el proyecto crece puede no ser suficiente. 驴Qu茅 pasa si alguien modifica nuestra funci贸n y se olvida de testear que funciona correctamente? Nuestra funci贸n habr铆a dejado de funcionar y nadie se habr铆a enterado.

Es aqu铆 donde los test autom谩ticos nos pueden ayudar. Python nos ofrece herramientas que nos permiten escribir tests que son ejecutados autom谩ticamente, y que si fallan dar谩n un error, alertando al programador de que ha 鈥渞oto鈥 algo. Podemos hacer esto con assert, donde identificamos dos partes claramente:

  • Por un lado tenemos la llamada a la funci贸n que queremos testear, que devuelve un resultado.
  • Por otro lado tenemos el resultado esperado, que comparamos con el resultado devuelto por la funci贸n. Si no es igual, se lanza un error.
assert(calcula_media([3, 7, 5]) == 5.0)
assert(calcula_media([30, 0]) == 15.0)

N贸tese que los valores de 5 y 15 los hemos calculado manualmente, y corresponden con la media de 3,7,5 y 30,0 respectivamente. Si por cualquier motivo alguien rompe nuestra funci贸n calcula_media(), cuando los tests se ejecuten lanzaran una excepci贸n.

Traceback (most recent call last):
  File "ejemplo.py", line 7, in <module>
    assert((calcula_media([30, 0]) == 15.0))
AssertionError

En proyectos grandes, es com煤n que antes de permitirnos hacer merge de nuestro c贸digo en master, se nos obligue a ejecutar un conjunto de tests automatizados. Si todos pasan, se asumir谩 que nuestro c贸digo funciona y que no hemos 鈥渞oto鈥 nada, por lo que tendremos el visto bueno.

Visto esto, tal vez pueda parecer que los test automatizados son lo mejor, sin embargo no siempre se pueden automatizar los tests. Si por ejemplo estamos trabajando con interfaces de usuario, es posible que no podamos automatizarlos, ya que se sigue necesitando a un humano que verifique los cambios visualmente.

Tests Unitarios en Python con unittest

Aunque el uso de assert() puede ser suficiente para nuestros tests, a veces se nos queda corto y necesitamos librer铆as como unittest, que ofrecen alguna que otra funcionalidad que nos har谩 la vida m谩s f谩cil. Veamos un ejemplo. Recordemos nuestra funci贸n calcula_media, que es la que queremos testear.

# funciones.py
def calcula_media(*args):
    return(sum(*args)/len(*args))

Podemos usar unittest para crear varios tests que verifiquen que nuestra funci贸n funciona correctamente. Aunque la estructura de un conjunto de tests se puede complicar m谩s, la estructura ser谩 siempre muy similar a la siguiente:

  • Creamos una clase Test<NombreDeLoQueSePrueba> que hereda de unittest.TestCase.
  • Definimos varios tests como m茅todos de la clase, usando test_<NombreDelTest> para nombrarlos.
  • En cada test ejecutamos las comprobaciones necesarias, usando assertEqual en vez de assert, pero su comportamiento es totalmente an谩logo.
# tests.py
from funciones import calcula_media
import unittest

class TestCalculaMedia(unittest.TestCase):
    def test_1(self):
        resultado = calcula_media([10, 10, 10])
        self.assertEqual(resultado, 10)

    def test_2(self):
        resultado = calcula_media([5, 3, 4])
        self.assertEqual(resultado, 4)

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

Si ejecutamos el c贸digo anterior, obtendremos el siguiente resultado. Esta es una de las ventajas de unittest, ya que nos muestra informaci贸n sobre los tests ejecutados, el tiempo que ha tardado y los resultados.

Ran 2 tests in 0.006s

OK

Por otro lado, usando -v podemos obtener m谩s informaci贸n sobre cada test ejecutado con su resultado individualmente. Si tenemos gran cantidad de tests suele ser recomendable usarla, ya que ser谩 m谩s f谩cil localizar los tests que han fallado.

$ python -m unittest -v tests

test_1 (tests.TestCalculaMedia) ... ok
test_2 (tests.TestCalculaMedia) ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

Por 煤ltimo, si tenemos varios ficheros de test, podemos usar discover, para decirle a Python que busque en la carpeta todos los tests y los ejecute.

$ python -m unittest discover -v

Otras comprobaciones en unittest

Anteriormente hemos visto el uso de .assertEqual(a, b) que simplemente verifica que dos valores a y b son iguales, y de lo contrario da error. Sin embargo unittest nos ofrece un amplio abanico de opciones. N贸tese que existen algunas variaciones usando 鈥渘ot鈥, como assertNotIn():

  • .assertEqual(a, b): Verifica la igualdad de ambos valores.
  • .assertTrue(x): Verifica que el valor es True.
  • .assertFalse(x): Verifica que el valor es False.
  • .assertIs(a, b): Verifica que ambas variables son la misma (ver operador is).
  • .assertIsNone(x): Verifica que el valor es None.
  • .assertIn(a, b): Verifica que a pertenece al iterable b (ver operador in).
  • .assertIsInstance(a, b): Verifica que a es una instancia de b
  • .assertRaises(x): Verifica que se lanza una excepci贸n.
import unittest
class TestEjemplos(unittest.TestCase):
    def test_in(self):
    	# Ok ya que 1 esta contenido en [1, 2, 3]
        self.assertIn(1, [1, 2, 3])

    def test_is(self):
        a = [1, 2, 3]
        b = a
        # Ok ya que son el mismo objeto
        self.assertIs(a, b)

    def test_excepcion(self):
    	# Dividir 0/0 da error, pero es lo esperado por el test
        with self.assertRaises(ZeroDivisionError):
            x = 0/0
$ python -m unittest -v tests

test_excepcion (tests.TestEjemplos) ... ok
test_in (tests.TestEjemplos) ... ok
test_is (tests.TestEjemplos) ... ok

----------------------------------------------------------------------
Ran 3 tests in 0.000s

OK

Usando setUp y tearDown

Otra de las ventajas de usar unittest, es que nos ofrece la posibilidad de definir funciones comunes que son ejecutadas antes y despu茅s de cada test. Estos m茅todos son setUp() y tearDown().

import unittest
class TestEjemplos(unittest.TestCase):
    def setUp(self):
        print("Entra setUp")
    def tearDown(self):
        print("Entra tearDown")

    def test_1(self):
        print("Test: test_1")
    def test_2(self):
        print("Test: test_2")

Siendo el resultado el siguiente. Podemos ver que hace una especie de sandwich con cada test, meti茅ndolo entre setUp y tearDown. N贸tese que si ambas funciones realizan siempre lo mismo, tal vez se pueda usar un TestSuite con una funci贸n com煤n para todos los tests, pero se trata de un concepto algo m谩s avanzado que dejaremos para otro art铆culo.

$ python -m unittest -v tests

test_1 (tests.TestEjemplos) ... Entra setUp
Test: test_1
Entra tearDown
ok
test_2 (tests.TestEjemplos) ... Entra setUp
Test: test_2
Entra tearDown
ok

----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

Evitando Side Effects

Hasta ahora hemos visto las herramientas que necesitamos para escribir nuestros tests, pero es tambi茅n muy importante seguir una serie de buenas practicas a la hora de escribir nuestro c贸digo. Uno de los principios m谩s importantes a seguir es el Principio de Responsabilidad 脷nica o SRP, que nos dice que el c贸digo (bien sea una clase o una funci贸n) debe tener una 煤nica responsabilidad. Si hace demasiadas cosas, ser谩 m谩s complicado de modificar y mantener, y adem谩s ser谩 m谩s complicado de testear.

Por lo tanto es tan importante escribir buenos tests que sean completos y tengan en cuenta todas las posibles casu铆sticas como escribir c贸digo que pueda ser testeado de manera f谩cil.