4. Primeras pruebas con unittest incluido en Python

4.1 Objetivo del tema

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.

Objetivo práctico: escribir las primeras pruebas automatizadas con unittest y ejecutarlas con python -m unittest.

4.2 Cuándo usar unittest

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.

  • No requiere instalación.
  • Funciona en cualquier instalación estándar de Python.
  • Organiza pruebas mediante clases.
  • Incluye métodos de aserción como assertEqual, assertTrue y assertRaises.
  • Puede ejecutarse desde la terminal con python -m unittest.

4.3 Crear una carpeta de práctica

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.

4.4 Crear el código que vamos a probar

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.

4.5 Crear el archivo de pruebas

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.

4.6 Escribir la primera 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.

4.7 Ejecutar la prueba

Desde la carpeta del proyecto, ejecuta:

python -m unittest

La salida esperada será similar a:

Salida esperada al ejecutar la primera prueba con unittest
.
----------------------------------------------------------------------
Ran 1 test in 0.001s

OK

El punto representa una prueba exitosa. El mensaje OK indica que todas las pruebas ejecutadas pasaron.

4.8 Agregar más pruebas

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.

4.9 Probar excepciones esperadas

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.

4.10 Archivo completo de pruebas

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.

4.11 Ejecutar nuevamente la suite

Ejecuta:

python -m unittest

La salida esperada:

......
----------------------------------------------------------------------
Ran 6 tests in 0.001s

OK

Ahora aparecen seis puntos porque se ejecutaron seis pruebas.

4.12 Ejecutar un archivo específico

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.

4.13 Ejecutar una clase específica

Para ejecutar una clase de prueba puntual:

python -m unittest test_cuentas.TestCuentas

El formato es archivo.Clase, sin la extensión .py.

4.14 Ejecutar una prueba específica

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.

4.15 Ver más detalle con verbose

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

4.16 Provocar una falla para aprender a leerla

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)

4.17 Diferencia entre falla y error

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.

4.18 Estructura final del proyecto

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.

4.19 Errores frecuentes

  • Olvidar heredar de unittest.TestCase: se pierden los métodos de aserción de unittest.
  • Nombrar métodos sin test_: la herramienta no los ejecuta como pruebas.
  • Confundir orden en assertEqual: conviene colocar primero el valor obtenido y luego el esperado, o mantener un criterio consistente.
  • No probar errores esperados: las validaciones también forman parte del comportamiento del código.
  • Ejecutar desde otra carpeta: puede provocar errores de importación.

4.20 Comandos usados en este tema

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

4.21 Qué debes recordar de este tema

  • unittest viene incluido con Python.
  • Las pruebas se escriben dentro de clases que heredan de unittest.TestCase.
  • Los métodos de prueba deben comenzar con test_.
  • assertEqual, assertTrue, assertFalse y assertRaises permiten verificar resultados.
  • python -m unittest descubre y ejecuta pruebas automáticamente.
  • Leer fallas y errores es parte esencial del trabajo con pruebas.

4.22 Conclusión

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.