Tests unitarios en Python: unittest, pytest y mocks

Publicado el: 11/07/2026
Tempo de leitura: 5 minutos
Exemplo de testes unitários em Python com código de unittest para validação automatizada

Los tests unitarios permiten comprobar automáticamente que una función, una clase o una pequeña parte de una aplicación produce el resultado esperado. En lugar de ejecutar el programa completo y revisar todo manualmente, puedes escribir casos que se repiten en segundos después de cada cambio. Esta práctica reduce regresiones, facilita las refactorizaciones y convierte los errores en información concreta.

Para aprovechar esta guía conviene conocer las funciones en Python, el manejo de excepciones, la organización de módulos y paquetes, los fundamentos de pytest y la automatización de scripts de Python.

Qué es un test unitario

Una unidad suele ser la parte más pequeña que puede probarse de forma aislada: una función, un método o una regla de negocio. Un buen test prepara datos, ejecuta la unidad y compara el resultado con una expectativa. Si la expectativa no se cumple, el test falla y muestra dónde investigar.

Supongamos esta función:

def calcular_total(precios, descuento=0):
    subtotal = sum(precios)
    return subtotal * (1 - descuento)

Un test sencillo puede verificar el caso normal y también los límites:

def test_calcular_total_sin_descuento():
    assert calcular_total([10, 20, 30]) == 60


def test_calcular_total_con_descuento():
    assert calcular_total([100], 0.20) == 80


def test_calcular_total_lista_vacia():
    assert calcular_total([]) == 0

Empezar con unittest

unittest forma parte de la biblioteca estándar. No necesitas instalar nada. La documentación oficial de unittest describe clases de prueba, aserciones, preparación y descubrimiento automático.

import unittest


class TestCalcularTotal(unittest.TestCase):
    def test_sin_descuento(self):
        self.assertEqual(calcular_total([10, 20]), 30)

    def test_con_descuento(self):
        self.assertAlmostEqual(calcular_total([100], 0.15), 85)


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

Los métodos deben comenzar con test_. Entre las aserciones más útiles están assertEqual, assertTrue, assertIsNone, assertIn y assertRaises. Para números decimales, assertAlmostEqual evita fallos causados por pequeñas diferencias de representación.

Probar excepciones

Los errores esperados también forman parte del contrato de una función:

def dividir(a, b):
    if b == 0:
        raise ValueError("El divisor no puede ser cero")
    return a / b


class TestDividir(unittest.TestCase):
    def test_division_normal(self):
        self.assertEqual(dividir(10, 2), 5)

    def test_divisor_cero(self):
        with self.assertRaises(ValueError):
            dividir(10, 0)

El test no debe ocultar una excepción inesperada. Debe confirmar únicamente el tipo de error que la función promete producir.

Usar pytest

Pytest permite escribir tests más compactos y ofrece fixtures, parametrización, plugins e informes claros. Instálalo dentro de un entorno virtual:

python -m pip install pytest
python -m pytest

La documentación oficial de pytest es la referencia principal para fixtures, parametrización y configuración.

import pytest


@pytest.mark.parametrize(
    "precios, descuento, esperado",
    [
        ([10, 20], 0, 30),
        ([100], 0.10, 90),
        ([], 0, 0),
    ],
)
def test_calcular_total(precios, descuento, esperado):
    assert calcular_total(precios, descuento) == esperado

La parametrización evita copiar el mismo test para muchos datos. Cada combinación aparece como un caso separado en el informe.

Fixtures para preparar datos

Una fixture crea recursos reutilizables, como usuarios, archivos temporales o conexiones de prueba:

import pytest


@pytest.fixture
def usuario():
    return {"nombre": "Ana", "activo": True}


def test_usuario_activo(usuario):
    assert usuario["activo"] is True

Para archivos, utiliza tmp_path, que crea una carpeta temporal aislada:

def guardar_texto(ruta, contenido):
    ruta.write_text(contenido, encoding="utf-8")


def test_guardar_texto(tmp_path):
    archivo = tmp_path / "salida.txt"
    guardar_texto(archivo, "hola")
    assert archivo.read_text(encoding="utf-8") == "hola"

Qué es un mock

Un mock sustituye una dependencia externa durante el test. Es útil para APIs, correo, reloj, sistema de archivos o bases de datos. El objetivo no es simular todo, sino aislar la lógica que realmente quieres comprobar.

from unittest.mock import patch


def obtener_temperatura(cliente):
    respuesta = cliente.get("/temperatura")
    return respuesta.json()["valor"]


def test_obtener_temperatura():
    with patch("mi_modulo.cliente") as cliente_falso:
        cliente_falso.get.return_value.json.return_value = {"valor": 24}
        assert obtener_temperatura(cliente_falso) == 24

Comprueba también que la dependencia recibió los argumentos correctos con assert_called_once_with. No abuses de mocks internos porque pueden hacer que el test confirme la implementación en vez del comportamiento.

Estructura recomendada

proyecto/
├── src/
│   └── tienda/
│       ├── __init__.py
│       └── precios.py
├── tests/
│   ├── test_precios.py
│   └── test_usuarios.py
└── pyproject.toml

Mantén los tests separados del código de producción, pero con nombres que permitan localizar rápidamente la unidad relacionada. Cada test debe ser independiente: su resultado no puede depender del orden de ejecución.

Patrón Arrange, Act, Assert

Una estructura clara divide el test en tres fases. Arrange prepara datos; Act ejecuta la operación; Assert verifica el resultado:

def test_aplicar_impuesto():
    # Arrange
    precio = 100
    tasa = 0.21

    # Act
    total = precio * (1 + tasa)

    # Assert
    assert total == 121

En tests pequeños no necesitas comentarios, pero la separación visual ayuda a comprender casos complejos.

Evitar tests frágiles

No pruebes detalles privados sin necesidad. Un test que depende del nombre de una variable interna se rompe aunque el comportamiento siga siendo correcto. Evita fechas reales, servicios externos y rutas absolutas. Congela o inyecta el reloj, usa archivos temporales y sustituye las llamadas de red.

Tampoco conviene generar datos aleatorios sin una semilla o sin registrar el caso que falló. Los tests deben ser deterministas: con el mismo código y entorno deben producir el mismo resultado.

Cobertura y calidad

La cobertura indica qué líneas fueron ejecutadas, pero no demuestra que las comprobaciones sean buenas. Un proyecto puede tener cobertura alta y aserciones débiles. Prioriza reglas importantes, límites, errores, entradas vacías, valores negativos y permisos.

python -m pip install pytest-cov
python -m pytest --cov=src --cov-report=term-missing

Ejecutar tests automáticamente

Ejecuta la suite antes de cada commit y también en integración continua. Un flujo básico instala dependencias y llama a python -m pytest. No guardes secretos reales en los tests. Usa variables específicas y servicios efímeros para bases de datos.

Errores frecuentes

Los fallos más comunes son probar varias ideas en un solo caso, usar datos compartidos mutables, depender de Internet, ignorar excepciones, comparar floats con igualdad exacta y escribir tests después de que aparece el error sin reproducir primero el problema. También es un error hacer que un test modifique archivos o bases de producción.

Conclusión

Los tests unitarios convierten el comportamiento esperado en una especificación ejecutable. Empieza con funciones pequeñas, añade casos normales y límites, prueba excepciones y utiliza fixtures para aislar recursos. unittest ofrece una solución estándar y pytest reduce el código repetitivo. Los mocks ayudan con dependencias externas, pero deben usarse con moderación. Una suite rápida, independiente y legible permite mejorar el proyecto con mucha más seguridad.

Compartilhe:

Facebook
WhatsApp
Twitter
LinkedIn

Contenido del artículo

    Artículos relacionados

    Proteção de API Flask usando autenticação JWT em Python
    Buenas Prácticas
    Foto de perfil de Leandro Hirt da Academify

    Cómo proteger una API Flask con JWT

    Protege una API Flask con JWT, access y refresh tokens, bcrypt, roles, revocación, variables de entorno, HTTPS y pruebas de

    Ler mais

    Tempo de leitura: 5 minutos
    11/07/2026
    Buenas Prácticas
    Foto de perfil de Leandro Hirt da Academify

    Enums en Python: evita valores mágicos y errores

    Aprende enums en Python con Enum, IntEnum, StrEnum, auto, unique, Flag, validación, JSON, bases de datos y buenas prácticas.

    Ler mais

    Tempo de leitura: 4 minutos
    11/07/2026
    Medição de tempo de execução de código Python com timeit
    Buenas Prácticas
    Foto de perfil de Leandro Hirt da Academify

    timeit en Python: mide el rendimiento correctamente

    Aprende timeit en Python para medir funciones, usar repeat y Timer, comparar implementaciones, reducir ruido y evitar benchmarks engañosos.

    Ler mais

    Tempo de leitura: 6 minutos
    11/07/2026
    Uso de dataclasses para simplificar classes em Python
    Buenas Prácticas
    Foto de perfil de Leandro Hirt da Academify

    Dataclasses en Python: clases de datos más limpias

    Aprende dataclasses en Python: campos, valores predeterminados, field, frozen, order, __post_init__, herencia, asdict y buenas prácticas.

    Ler mais

    Tempo de leitura: 4 minutos
    11/07/2026
    Criação de hashes seguros para senhas usando Python
    Buenas Prácticas
    Foto de perfil de Leandro Hirt da Academify

    Hash de contraseñas con bcrypt en Python

    Aprende bcrypt en Python para almacenar contraseñas: salt, coste, hash, verificación, rehash, límites, errores y recomendaciones de seguridad.

    Ler mais

    Tempo de leitura: 5 minutos
    11/07/2026
    Problemas de travamento com threading em scripts Python
    Buenas Prácticas
    Foto de perfil de Leandro Hirt da Academify

    Threading en Python: evita que tus scripts se bloqueen

    Aprende threading en Python: hilos, tareas de entrada y salida, join, locks, colas, ThreadPoolExecutor, errores y límites del GIL.

    Ler mais

    Tempo de leitura: 4 minutos
    11/07/2026