34. Errores frecuentes al testear en Python y cómo corregirlos

34.1 Objetivo del tema

Después de aprender herramientas y técnicas de testing, conviene revisar errores habituales. Muchos problemas no aparecen por falta de sintaxis, sino por pruebas difíciles de mantener, lentas o poco confiables.

En este tema veremos fallas frecuentes al testear en Python y formas concretas de corregirlas.

Idea clave: una buena prueba debe ser clara, repetible, rápida y útil cuando falla.

34.2 Crear una carpeta de práctica

Crea un proyecto nuevo:

mkdir errores-testing-demo
cd errores-testing-demo

Instala pytest:

python -m pip install pytest

34.3 Crear estructura base

Crea estos archivos:

mkdir src
mkdir src\tienda
mkdir tests
New-Item src\tienda\__init__.py -ItemType File
New-Item src\tienda\pedidos.py -ItemType File
New-Item tests\test_pedidos.py -ItemType File

Agrega un pytest.ini para simplificar imports:

[pytest]
pythonpath = src
testpaths = tests
addopts = -ra

34.4 Error 1: probar demasiadas cosas en una sola prueba

Una prueba con muchas verificaciones es más difícil de diagnosticar cuando falla:

def test_pedido_completo():
    pedido = crear_pedido("Ana")
    agregar_producto(pedido, "Teclado", 30000)
    agregar_producto(pedido, "Mouse", 12000)
    assert pedido["cliente"] == "Ana"
    assert len(pedido["productos"]) == 2
    assert calcular_total(pedido) == 42000
    assert pedido["estado"] == "pendiente"

Si falla, hay que leer mucho contexto para entender qué comportamiento se rompió.

34.5 Corrección: separar comportamientos

Conviene escribir pruebas más enfocadas:

def test_crear_pedido_guarda_cliente():
    pedido = crear_pedido("Ana")

    assert pedido["cliente"] == "Ana"


def test_agregar_producto_aumenta_la_lista():
    pedido = crear_pedido("Ana")

    agregar_producto(pedido, "Teclado", 30000)

    assert len(pedido["productos"]) == 1


def test_calcular_total_suma_productos():
    pedido = crear_pedido("Ana")
    agregar_producto(pedido, "Teclado", 30000)
    agregar_producto(pedido, "Mouse", 12000)

    assert calcular_total(pedido) == 42000

Cada prueba tiene una intención clara y un motivo de falla más fácil de interpretar.

34.6 Error 2: depender del orden de ejecución

Las pruebas no deben depender de que otra prueba haya corrido antes:

productos = []


def test_agregar_producto():
    productos.append("Teclado")
    assert len(productos) == 1


def test_lista_con_producto():
    assert productos == ["Teclado"]

Este ejemplo es frágil porque comparte estado global entre pruebas.

34.7 Corrección: crear datos propios en cada prueba

Cada prueba debe preparar lo que necesita:

def test_agregar_producto():
    productos = []

    productos.append("Teclado")

    assert productos == ["Teclado"]


def test_lista_inicialmente_vacia():
    productos = []

    assert productos == []

Así las pruebas son independientes y pueden ejecutarse en cualquier orden.

34.8 Error 3: usar datos reales externos

Una prueba que depende de internet, una API real o una base de datos compartida puede fallar por motivos ajenos al código:

import requests


def obtener_dolar():
    respuesta = requests.get("https://api.example.com/dolar")
    return respuesta.json()["valor"]

Si la red falla o el servicio cambia, la prueba deja de ser confiable.

34.9 Corrección: aislar dependencias externas

Se puede separar la lógica para probarla sin llamar a servicios reales:

def extraer_valor_dolar(datos):
    return datos["valor"]

Y probar esa función con datos controlados:

def test_extraer_valor_dolar():
    datos = {"valor": 1200}

    assert extraer_valor_dolar(datos) == 1200

Las pruebas de integración con servicios reales deben ser menos frecuentes y estar claramente separadas.

34.10 Error 4: abusar de mocks

Los mocks son útiles, pero demasiados mocks pueden probar la implementación en lugar del comportamiento:

def test_crear_factura_con_demasiados_mocks(mocker):
    repo = mocker.Mock()
    calculador = mocker.Mock()
    notificador = mocker.Mock()
    logger = mocker.Mock()
    calculador.total.return_value = 1000

    crear_factura(repo, calculador, notificador, logger)

    repo.guardar.assert_called_once()
    calculador.total.assert_called_once()
    notificador.enviar.assert_called_once()
    logger.info.assert_called_once()

Una prueba así se rompe fácilmente si cambia la organización interna, aunque el resultado para el usuario siga siendo correcto.

34.11 Corrección: verificar resultados observables

Siempre que sea posible, verifica el resultado producido:

def test_crear_factura_calcula_total():
    pedido = {
        "productos": [
            {"nombre": "Teclado", "precio": 30000},
            {"nombre": "Mouse", "precio": 12000},
        ]
    }

    factura = crear_factura(pedido)

    assert factura["total"] == 42000

Usa mocks sobre todo para cortar dependencias externas, no para comprobar cada paso interno.

34.12 Error 5: nombres poco claros

Un nombre como este no ayuda a entender la intención:

def test_1():
    assert aplicar_descuento(1000, 10) == 900

Cuando la suite crece, nombres genéricos vuelven difícil encontrar qué comportamiento falló.

34.13 Corrección: describir el caso probado

Un nombre más claro documenta el comportamiento:

def test_aplicar_descuento_resta_porcentaje_al_precio():
    assert aplicar_descuento(1000, 10) == 900

No hace falta escribir nombres larguísimos, pero sí conviene que indiquen qué se está verificando.

34.14 Error 6: no probar casos borde

Probar solo el caso feliz deja huecos importantes:

def test_aplicar_descuento():
    assert aplicar_descuento(1000, 10) == 900

Faltan casos como descuento cero, descuento del 100%, precio negativo o porcentaje inválido.

34.15 Corrección: agregar casos borde y datos inválidos

Podemos usar parametrización:

import pytest


@pytest.mark.parametrize(
    "precio, porcentaje, esperado",
    [
        (1000, 0, 1000),
        (1000, 10, 900),
        (1000, 100, 0),
    ],
)
def test_aplicar_descuento_con_porcentajes_validos(precio, porcentaje, esperado):
    assert aplicar_descuento(precio, porcentaje) == esperado


@pytest.mark.parametrize("porcentaje", [-1, 101])
def test_aplicar_descuento_rechaza_porcentaje_invalido(porcentaje):
    with pytest.raises(ValueError):
        aplicar_descuento(1000, porcentaje)

34.16 Error 7: asserts demasiado débiles

Un assert débil puede pasar aunque el resultado sea incorrecto:

def test_crear_usuario():
    usuario = crear_usuario("Ana", "ana@example.com")

    assert usuario

La prueba solo verifica que hay algún valor, no que el usuario se haya creado correctamente.

34.17 Corrección: verificar campos importantes

Conviene comprobar el resultado relevante:

def test_crear_usuario_guarda_nombre_y_email():
    usuario = crear_usuario("Ana", "ana@example.com")

    assert usuario["nombre"] == "Ana"
    assert usuario["email"] == "ana@example.com"
    assert usuario["activo"] is True

34.18 Error 8: usar sleeps en pruebas

Los sleep vuelven lenta la suite y no siempre eliminan fallas intermitentes:

import time


def test_proceso_lento():
    iniciar_proceso()
    time.sleep(5)

    assert proceso_finalizado()

Esta prueba tarda como mínimo cinco segundos y puede seguir fallando si el proceso tarda más.

34.19 Corrección: controlar el tiempo o consultar estado

Si el código depende del tiempo, conviene aislar esa dependencia o usar una espera con límite y condición:

def esperar_hasta(condicion, intentos=10):
    for _ in range(intentos):
        if condicion():
            return True
    return False


def test_proceso_finaliza():
    iniciar_proceso()

    assert esperar_hasta(proceso_finalizado)

En proyectos reales también se puede inyectar una función de reloj o usar herramientas específicas para controlar el tiempo.

34.20 Error 9: ignorar mensajes de falla

Cuando pytest muestra una falla, no conviene mirar solo la última línea. La salida suele indicar archivo, función, valores comparados y excepción.

python -m pytest -vv

La opción -vv muestra más detalle sobre cada prueba ejecutada.

34.21 Error 10: medir cobertura sin interpretar el resultado

Tener alta cobertura no garantiza buenas pruebas. Una prueba puede ejecutar muchas líneas sin verificar lo importante:

def test_generar_reporte():
    generar_reporte([1, 2, 3])

Esta prueba aumenta cobertura, pero no comprueba el contenido del reporte.

34.22 Corrección: usar cobertura como señal, no como objetivo único

Una prueba más útil verifica el resultado:

def test_generar_reporte_incluye_total():
    reporte = generar_reporte([10, 20, 30])

    assert "Total: 60" in reporte

La cobertura ayuda a encontrar zonas sin pruebas, pero la calidad depende de las verificaciones.

34.23 Error 11: mezclar pruebas unitarias e integración sin marcar

Si una suite mezcla pruebas rápidas con pruebas lentas o externas, el equipo puede dejar de ejecutarla con frecuencia.

Conviene marcar las pruebas especiales:

import pytest


@pytest.mark.integration
def test_guardar_pedido_en_base_de_datos():
    resultado = guardar_pedido_real()

    assert resultado["ok"] is True

Y registrar el marcador en pytest.ini:

[pytest]
markers =
    integration: pruebas que usan servicios reales o infraestructura externa

34.24 Ejecutar solo pruebas rápidas

Si registraste marcadores, puedes excluir integraciones durante el desarrollo diario:

python -m pytest -m "not integration"

Y ejecutar integraciones cuando corresponda:

python -m pytest -m integration

34.25 Error 12: no ejecutar la suite completa antes de integrar cambios

Ejecutar solo la prueba que estás corrigiendo es útil durante el desarrollo, pero antes de cerrar un cambio conviene correr toda la suite:

python -m pytest

Si el proyecto usa formateo y análisis estático, también conviene ejecutar:

python -m black --check src tests
python -m ruff check src tests
python -m pytest

34.26 Lista de revisión rápida

  • ¿La prueba tiene un nombre claro?
  • ¿Prepara sus propios datos?
  • ¿Verifica un comportamiento concreto?
  • ¿Evita depender de internet, hora actual o archivos compartidos?
  • ¿Falla con un mensaje fácil de entender?
  • ¿Puede ejecutarse junto con el resto de la suite?

34.27 Comandos usados en este tema

mkdir errores-testing-demo
cd errores-testing-demo
python -m pip install pytest
mkdir src
mkdir src\tienda
mkdir tests
New-Item src\tienda\__init__.py -ItemType File
New-Item src\tienda\pedidos.py -ItemType File
New-Item tests\test_pedidos.py -ItemType File
python -m pytest
python -m pytest -vv
python -m pytest -m "not integration"
python -m pytest -m integration
python -m black --check src tests
python -m ruff check src tests

34.28 Qué debes recordar de este tema

  • Las pruebas deben ser independientes entre sí.
  • Conviene probar comportamientos concretos y resultados observables.
  • Los datos externos y el tiempo pueden volver frágil una suite.
  • Los mocks se usan para aislar dependencias, no para probar cada línea interna.
  • La cobertura es una señal útil, pero no reemplaza buenos asserts.
  • Antes de integrar cambios, ejecuta la suite completa.

34.29 Conclusión

En este tema repasamos errores frecuentes al testear en Python: pruebas demasiado grandes, dependientes del orden, acopladas a servicios reales, con mocks excesivos, nombres poco claros, asserts débiles y cobertura mal interpretada.

En el próximo tema construiremos un caso práctico integrador con un proyecto Python y una suite completa de pruebas.