unittest es el módulo de testing incluido en la biblioteca estándar de Python. Esto significa que podemos escribir y ejecutar pruebas sin instalar herramientas adicionales.
En este tema crearemos un proyecto pequeño, escribiremos funciones para probar, construiremos una clase de prueba con unittest.TestCase, ejecutaremos las pruebas desde la terminal y veremos cómo interpretar resultados exitosos y fallidos.
unittest es útil cuando queremos trabajar solamente con herramientas incluidas en Python o cuando mantenemos proyectos que ya usan este estilo de pruebas.
Aunque en muchos proyectos modernos se usa pytest, conocer unittest sigue siendo importante porque aparece en documentación, proyectos existentes, bibliotecas y ejemplos oficiales.
assertEqual, assertTrue y assertRaises.python -m unittest.Crea una carpeta nueva para este tema:
mkdir unittest-demo
cd unittest-demo
Como unittest viene incluido con Python, no necesitamos instalar paquetes para este primer ejemplo.
Crea un archivo llamado cuentas.py:
def calcular_saldo(inicial, deposito, extraccion):
if inicial < 0:
raise ValueError("El saldo inicial no puede ser negativo")
if deposito < 0:
raise ValueError("El depósito no puede ser negativo")
if extraccion < 0:
raise ValueError("La extracción no puede ser negativa")
saldo = inicial + deposito - extraccion
if saldo < 0:
raise ValueError("El saldo no puede quedar negativo")
return saldo
def esta_activa(saldo):
return saldo > 0
Este módulo tiene dos funciones. Una calcula un saldo y valida datos inválidos. La otra indica si una cuenta está activa según su saldo.
Crea un archivo llamado test_cuentas.py. Por convención, el nombre comienza con test_ para que la herramienta pueda descubrirlo fácilmente.
import unittest
from cuentas import calcular_saldo, esta_activa
class TestCuentas(unittest.TestCase):
pass
La clase TestCuentas hereda de unittest.TestCase. Dentro de esa clase escribiremos métodos de prueba.
Agrega un método dentro de la clase:
import unittest
from cuentas import calcular_saldo, esta_activa
class TestCuentas(unittest.TestCase):
def test_calcular_saldo_con_deposito_y_extraccion(self):
resultado = calcular_saldo(1000, 500, 300)
self.assertEqual(resultado, 1200)
El nombre del método empieza con test_. Esa convención indica que el método es una prueba.
self.assertEqual(resultado, 1200) compara el valor obtenido con el valor esperado. Si son iguales, la prueba pasa. Si son distintos, la prueba falla.
Desde la carpeta del proyecto, ejecuta:
python -m unittest
La salida esperada será similar a:
.
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK
El punto representa una prueba exitosa. El mensaje OK indica que todas las pruebas ejecutadas pasaron.
Podemos agregar varias pruebas dentro de la misma clase:
import unittest
from cuentas import calcular_saldo, esta_activa
class TestCuentas(unittest.TestCase):
def test_calcular_saldo_con_deposito_y_extraccion(self):
resultado = calcular_saldo(1000, 500, 300)
self.assertEqual(resultado, 1200)
def test_calcular_saldo_sin_movimientos(self):
resultado = calcular_saldo(800, 0, 0)
self.assertEqual(resultado, 800)
def test_cuenta_con_saldo_positivo_esta_activa(self):
resultado = esta_activa(100)
self.assertTrue(resultado)
def test_cuenta_con_saldo_cero_no_esta_activa(self):
resultado = esta_activa(0)
self.assertFalse(resultado)
Cada método verifica un comportamiento concreto. Esto hace que, cuando algo falle, podamos identificar el problema con mayor precisión.
También debemos probar qué ocurre con datos inválidos. Para eso usamos assertRaises:
def test_saldo_inicial_negativo_lanza_error(self):
with self.assertRaises(ValueError):
calcular_saldo(-100, 0, 0)
Esta prueba espera que la función lance ValueError. Si la excepción ocurre, la prueba pasa. Si no ocurre, la prueba falla.
El archivo test_cuentas.py puede quedar así:
import unittest
from cuentas import calcular_saldo, esta_activa
class TestCuentas(unittest.TestCase):
def test_calcular_saldo_con_deposito_y_extraccion(self):
resultado = calcular_saldo(1000, 500, 300)
self.assertEqual(resultado, 1200)
def test_calcular_saldo_sin_movimientos(self):
resultado = calcular_saldo(800, 0, 0)
self.assertEqual(resultado, 800)
def test_cuenta_con_saldo_positivo_esta_activa(self):
resultado = esta_activa(100)
self.assertTrue(resultado)
def test_cuenta_con_saldo_cero_no_esta_activa(self):
resultado = esta_activa(0)
self.assertFalse(resultado)
def test_saldo_inicial_negativo_lanza_error(self):
with self.assertRaises(ValueError):
calcular_saldo(-100, 0, 0)
def test_extraccion_mayor_al_saldo_lanza_error(self):
with self.assertRaises(ValueError):
calcular_saldo(100, 0, 200)
if __name__ == "__main__":
unittest.main()
El bloque final permite ejecutar el archivo directamente con python test_cuentas.py, aunque en el curso preferiremos usar python -m unittest.
Ejecuta:
python -m unittest
La salida esperada:
......
----------------------------------------------------------------------
Ran 6 tests in 0.001s
OK
Ahora aparecen seis puntos porque se ejecutaron seis pruebas.
También puedes ejecutar solamente un archivo de pruebas:
python -m unittest test_cuentas.py
Esto es útil cuando el proyecto tiene muchos archivos y queremos enfocarnos en uno.
Para ejecutar una clase de prueba puntual:
python -m unittest test_cuentas.TestCuentas
El formato es archivo.Clase, sin la extensión .py.
Para ejecutar un solo método de prueba:
python -m unittest test_cuentas.TestCuentas.test_calcular_saldo_con_deposito_y_extraccion
El formato completo es archivo.Clase.metodo.
La opción -v muestra el nombre de cada prueba ejecutada:
python -m unittest -v
Salida posible:
test_calcular_saldo_con_deposito_y_extraccion ... ok
test_calcular_saldo_sin_movimientos ... ok
test_cuenta_con_saldo_cero_no_esta_activa ... ok
test_cuenta_con_saldo_positivo_esta_activa ... ok
test_extraccion_mayor_al_saldo_lanza_error ... ok
test_saldo_inicial_negativo_lanza_error ... ok
----------------------------------------------------------------------
Ran 6 tests in 0.001s
OK
Cambia temporalmente una expectativa para que sea incorrecta:
def test_calcular_saldo_sin_movimientos(self):
resultado = calcular_saldo(800, 0, 0)
self.assertEqual(resultado, 700)
Ejecuta:
python -m unittest
Verás una falla similar a:
FAIL: test_calcular_saldo_sin_movimientos (test_cuentas.TestCuentas)
AssertionError: 800 != 700
El mensaje indica la prueba que falló y muestra la diferencia entre el valor obtenido y el valor esperado. Luego vuelve a dejar la expectativa correcta:
self.assertEqual(resultado, 800)
En unittest, una falla y un error no significan exactamente lo mismo:
| Resultado | Significado | Ejemplo |
|---|---|---|
| Falla | La prueba se ejecutó, pero una aserción no se cumplió. | Esperábamos 700 y la función devolvió 800. |
| Error | La prueba no pudo completarse por una excepción no esperada. | Nombre de función mal escrito o importación incorrecta. |
La carpeta debería quedar así:
unittest-demo/
|-- cuentas.py
`-- test_cuentas.py
En proyectos más grandes usaremos carpetas separadas para el código y las pruebas, pero para este primer contacto con unittest esta estructura es suficiente.
unittest.TestCase: se pierden los métodos de aserción de unittest.test_: la herramienta no los ejecuta como pruebas.assertEqual: conviene colocar primero el valor obtenido y luego el esperado, o mantener un criterio consistente.mkdir unittest-demo
cd unittest-demo
python -m unittest
python -m unittest test_cuentas.py
python -m unittest test_cuentas.TestCuentas
python -m unittest test_cuentas.TestCuentas.test_calcular_saldo_con_deposito_y_extraccion
python -m unittest -v
python test_cuentas.py
unittest viene incluido con Python.unittest.TestCase.test_.assertEqual, assertTrue, assertFalse y assertRaises permiten verificar resultados.python -m unittest descubre y ejecuta pruebas automáticamente.En este tema escribimos las primeras pruebas con unittest. Creamos una clase de prueba, agregamos métodos con nombres adecuados, usamos aserciones y ejecutamos las pruebas desde la terminal.
En el próximo tema profundizaremos en las formas de ejecutar pruebas con python -m unittest, incluyendo descubrimiento automático, ejecución por carpeta, ejecución por archivo y opciones útiles.