Testing con unittest

Intermedio
Python
Python
Actualizado: 05/05/2026

Estructura básica de un test con unittest

flowchart TD
    UT["unittest (stdlib)"] --> TC["TestCase: hereda casos"]
    TC --> SETUP["setUp / tearDown: por test"]
    TC --> CSETUP["setUpClass / tearDownClass"]
    TC --> METHODS["test_* métodos: descubrimiento automático"]
    METHODS --> ASSERT["assertEqual, assertTrue, assertRaises"]
    UT --> RUN["unittest.main() / discover"]
    UT --> MOCK["unittest.mock + @patch: dobles de prueba"]
    UT -.->|alternativa moderna| PYTEST["pytest (más simple)"]

El módulo unittest organiza las pruebas como clases que heredan de unittest.TestCase. Cada método cuyo nombre empieza por test_ se considera un caso de prueba independiente.

import unittest

def sumar(a: int, b: int) -> int:
    return a + b

class TestSumar(unittest.TestCase):
    def test_suma_positivos(self) -> None:
        self.assertEqual(sumar(2, 3), 5)

    def test_suma_con_cero(self) -> None:
        self.assertEqual(sumar(0, 7), 7)

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

La ejecución desde la terminal usa el descubrimiento automático que recorre los archivos test_*.py:

python -m unittest discover -s tests -p "test_*.py"

Los métodos de aserción viven en TestCase y cubren las comparaciones habituales, assertEqual, assertNotEqual, assertTrue, assertFalse, assertIn, assertRaises, assertAlmostEqual. Usarlos en lugar de un assert puro da mensajes de error más claros.

TestCase también ofrece subTest, un gestor de contexto que permite parametrizar dentro de un único método sin dejar pruebas huérfanas cuando una variante falla.

Hooks de preparación y limpieza

Cada TestCase admite métodos de ciclo de vida que se ejecutan antes y después de cada test o de la clase completa. Son equivalentes a las fixtures de pytest, aunque más rígidos.

import unittest
from pathlib import Path

class TestArchivo(unittest.TestCase):
    @classmethod
    def setUpClass(cls) -> None:
        cls.directorio = Path("./tmp_tests")
        cls.directorio.mkdir(exist_ok=True)

    def setUp(self) -> None:
        self.archivo = self.directorio / "datos.txt"
        self.archivo.write_text("contenido")

    def test_lectura(self) -> None:
        self.assertEqual(self.archivo.read_text(), "contenido")

    def tearDown(self) -> None:
        self.archivo.unlink(missing_ok=True)

    @classmethod
    def tearDownClass(cls) -> None:
        cls.directorio.rmdir()

setUpClass y tearDownClass se ejecutan una sola vez para toda la clase, son útiles para recursos caros, conexiones o directorios compartidos. setUp y tearDown se ejecutan alrededor de cada test, garantizando aislamiento entre casos.

Mocks y doubles con unittest.mock

unittest.mock proporciona una API para sustituir colaboradores por objetos falsos controlados. Evita depender de servicios reales (red, bases de datos, reloj) durante los tests.

from unittest import TestCase
from unittest.mock import Mock, patch

class ClienteHTTP:
    def obtener(self, url: str) -> dict:
        raise NotImplementedError

def recuperar_usuario(cliente: ClienteHTTP, uid: int) -> str:
    datos = cliente.obtener(f"/usuarios/{uid}")
    return datos["nombre"]

class TestRecuperarUsuario(TestCase):
    def test_devuelve_nombre(self) -> None:
        cliente = Mock(spec=ClienteHTTP)
        cliente.obtener.return_value = {"nombre": "Ana"}

        nombre = recuperar_usuario(cliente, 1)

        self.assertEqual(nombre, "Ana")
        cliente.obtener.assert_called_once_with("/usuarios/1")

El decorador patch permite reemplazar temporalmente un objeto en su lugar de importación. Se recomienda parchear donde se usa, no donde se define.

from datetime import datetime

def hoy_iso() -> str:
    return datetime.now().isoformat()

class TestHoyIso(TestCase):
    @patch("modulo_bajo_test.datetime")
    def test_formato_fijo(self, mock_datetime) -> None:
        mock_datetime.now.return_value = datetime(2026, 1, 1, 12, 0, 0)
        self.assertEqual(hoy_iso(), "2026-01-01T12:00:00")

Mock admite spec=ClienteHTTP para que respete la firma del objeto real. Sin spec, el mock acepta cualquier método y puedes tener tests que pasan verdes pero el código real fallaría.

Para rutas de más de un colaborador, patch.multiple o la anotación en clase aplican el mismo patrón a varios tests a la vez, manteniendo la limpieza automática cuando el test termina.

Caso B2B: testing en proyectos heredados de banca y AAPP

En entidades bancarias y administraciones públicas con código Python heredado de hace una década, las suites de tests suelen estar escritas con unittest. Vuestro equipo se encuentra con esos proyectos en mantenimiento y conviene dominar la API estándar antes de plantear migración a pytest. La organización gana porque no hay que añadir dependencias externas en entornos donde la política de seguridad exige minimizar la superficie de paquetes (cada pip install en producción dispara un proceso de aprobación).

En telco con plataformas de billing donde la trazabilidad regulatoria exige auditar el linaje de cada versión de tests, la decisión de mantenerse en unittest (parte de stdlib) reduce el riesgo de que un nuevo release de una librería externa rompa la suite. Vuestro equipo paga este "coste" en verbosidad sintáctica a cambio de estabilidad multianual.

En administraciones públicas con entornos aislados (sin acceso a PyPI desde producción), unittest y unittest.mock cubren el 90% de los casos sin necesidad de descargar paquetes adicionales. Esto simplifica el proceso de despliegue en CPD propios o en infraestructura sometida al ENS Alto.

En retail con equipos junior recién incorporados, la curva de aprendizaje de unittest es menor para perfiles que vienen de Java/JUnit. Cuando el equipo está mixto Java+Python, la familiaridad sintáctica acelera la productividad inicial.

Versiones (2025)

unittest está en la stdlib desde Python 2.1 (2001) e inspirado por JUnit (de ahí xUnit). unittest.mock se incorporó como módulo estándar en Python 3.3 (2012). Python 3.13 (octubre 2024) y 3.14 (en desarrollo) lo mantienen sin cambios disruptivos. La capacidad subTest (Python 3.4+) permite parametrización ligera.

Para extensiones modernas, considerad pytest con su plugin pytest-unittest que ejecuta TestCase nativos sin reescritura, permitiendo migración gradual.

Anti-patrones y pitfalls

Asertos assert puros en lugar de self.assertEqual. Funcionan pero pierden los mensajes contextuales (assertEqual muestra ambos valores, assert solo muestra el lado izquierdo). En CI con feedback rico, los mensajes específicos importan.

Compartir estado entre tests con atributos de clase mutables. Si setUp no resetea estado y un test contamina un atributo de clase, los demás tests pueden pasar o fallar según el orden de ejecución. Usad siempre setUp para inicializar estado por test.

patch en el lugar equivocado. La regla es parchear donde se usa, no donde se define. Si vuestro código importa from external import api y vosotros parcheáis external.api, el parche no se aplica porque la referencia local en el módulo bajo test ya está resuelta. Parchead mi_modulo.api.

Mock sin spec. Sin spec=ClienteHTTP, el mock acepta cliente.metodo_inexistente() sin error. Esto genera tests verdes que fallan en producción cuando el método real no existe. Usad siempre spec o spec_set.

Tests que dependen de orden alfabético. unittest ejecuta tests en orden alfabético del nombre del método, no por código. Test que dependen del orden son frágiles ante refactor que renombra métodos.

Mezclar unittest y pytest sin comprender precedencia. pytest reconoce TestCase pero algunas fixtures pytest no son compatibles con métodos setUp. Para migración, optad por uno u otro estilo en cada archivo.

Comparativa con alternativas

Frente a pytest, unittest es más verboso (clases, self.assertEqual en lugar de assert) pero no requiere dependencia. pytest gana en sintaxis (assert n == 5), fixtures con @pytest.fixture y plugins. Para nuevo código, pytest es la elección por defecto. Para mantener código heredado, unittest permanece relevante.

Frente a nose / nose2, ambos están deprecados o casi sin mantenimiento. No los uséis para nuevo código.

Frente a doctest (también stdlib), unittest es para tests estructurados; doctest es para validar ejemplos en docstrings. Son complementarios, no alternativos.

Frente a hypothesis (property-based), unittest y hypothesis se combinan: hypothesis genera casos automáticamente y los pasa a métodos TestCase.

Documentación oficial

La referencia es la sección "unittest — Unit testing framework" del Python Standard Library Reference (docs.python.org/3/library/unittest.html) y "unittest.mock — mock object library" (docs.python.org/3/library/unittest.mock.html). El libro "Python Testing with unittest, nose, pytest" de Brian Okken cubre los tres frameworks con ejemplos comparativos.

unittest sigue siendo una opción válida para proyectos B2B con políticas estrictas sobre dependencias externas o con código heredado. Para vuestro equipo en proyectos nuevos, pytest es preferible por concisión, pero conocer unittest permite operar con cualquier base de código Python existente.

Alan Sastre - Autor del tutorial

Alan Sastre

Ingeniero de Software y formador, CEO en CertiDevs

Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Python es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.

Más tutoriales de Python

Explora más contenido relacionado con Python y continúa aprendiendo con nuestros tutoriales gratuitos.

Aprendizajes de esta lección

Escribir casos de prueba con TestCase y los métodos assert. Organizar el entorno de pruebas con setUp, tearDown y sus variantes de clase. Ejecutar suites con unittest.main y el descubrimiento automático. Sustituir dependencias con unittest.mock y los decoradores patch.