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.
Crea un proyecto nuevo:
mkdir errores-testing-demo
cd errores-testing-demo
Instala pytest:
python -m pip install pytest
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
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ó.
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.
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.
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.
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.
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.
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.
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.
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ó.
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.
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.
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)
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.
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
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.
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.
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.
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.
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.
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
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
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
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
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.