En este tema vamos a integrar lo aprendido durante el curso: medición de cobertura, lectura de reportes, cobertura de ramas, parametrización, configuración y mejora progresiva de pruebas.
Trabajaremos sobre un pequeño proyecto de tienda con reglas de precios, cupones y pagos. El objetivo no es llegar a un número perfecto, sino aplicar una estrategia razonable para mejorar cobertura y calidad.
Usaremos esta estructura:
cobertura-demo/
|-- pyproject.toml
|-- requirements.txt
|-- src/
| `-- tienda/
| |-- __init__.py
| |-- precios.py
| |-- cupones.py
| `-- pagos.py
`-- tests/
|-- test_precios.py
`-- test_cupones.py
El módulo pagos.py todavía no tendrá pruebas suficientes. Lo usaremos para practicar la mejora guiada por cobertura.
En pyproject.toml, deja una configuración consistente:
[tool.coverage.run]
source = ["src"]
branch = true
omit = [
"*/__init__.py",
]
[tool.coverage.report]
show_missing = true
precision = 2
fail_under = 80
Esta configuración mide src, activa ramas, omite inicializadores vacíos y muestra líneas faltantes.
Crea src/tienda/pagos.py:
def autorizar_pago(monto, metodo, intentos_fallidos=0):
if monto <= 0:
raise ValueError("El monto debe ser mayor que cero")
if intentos_fallidos >= 3:
return "bloqueado"
if metodo == "tarjeta":
if monto > 100000:
return "revision"
return "aprobado"
if metodo == "transferencia":
return "pendiente"
return "rechazado"
def calcular_comision(monto, metodo):
if monto <= 0:
raise ValueError("El monto debe ser mayor que cero")
if metodo == "tarjeta":
return monto * 0.03
if metodo == "transferencia":
return 0
raise ValueError("Método de pago inválido")
Este módulo tiene validaciones, ramas anidadas, retornos alternativos y excepciones.
Crea tests/test_pagos.py con pocas pruebas iniciales:
from tienda.pagos import autorizar_pago, calcular_comision
def test_autorizar_pago_tarjeta_aprobado():
assert autorizar_pago(1000, "tarjeta") == "aprobado"
def test_calcular_comision_tarjeta():
assert calcular_comision(1000, "tarjeta") == 30
Estas pruebas cubren el camino feliz de tarjeta, pero dejan varios comportamientos sin verificar.
Ejecuta el reporte:
En Windows PowerShell:
$env:PYTHONPATH="src"
python -m pytest --cov --cov-report=term-missing
En Linux o macOS:
PYTHONPATH=src python -m pytest --cov --cov-report=term-missing
Una salida posible para pagos.py:
Name Stmts Miss Branch BrPart Cover Missing
------------------------------------------------------------------
src\tienda\pagos.py 24 10 12 5 55.56% 3, 6, 10, 14, 16, 21, 27, 29, 31
El reporte muestra líneas faltantes y ramas parciales. Ahora debemos convertir esa información en pruebas útiles.
Al leer pagos.py, encontramos estos comportamientos sin cubrir:
Esta lista es más útil que una lista de números de línea porque está expresada como comportamientos.
Agrega pruebas para los caminos pendientes de autorizar_pago:
import pytest
from tienda.pagos import autorizar_pago, calcular_comision
def test_autorizar_pago_rechaza_monto_invalido():
with pytest.raises(ValueError, match="mayor que cero"):
autorizar_pago(0, "tarjeta")
def test_autorizar_pago_bloqueado_por_intentos():
assert autorizar_pago(1000, "tarjeta", intentos_fallidos=3) == "bloqueado"
def test_autorizar_pago_tarjeta_monto_alto_requiere_revision():
assert autorizar_pago(150000, "tarjeta") == "revision"
def test_autorizar_pago_transferencia_pendiente():
assert autorizar_pago(1000, "transferencia") == "pendiente"
def test_autorizar_pago_metodo_desconocido_rechazado():
assert autorizar_pago(1000, "efectivo") == "rechazado"
Cada prueba representa una regla específica del módulo.
Completa los caminos de calcular_comision:
def test_calcular_comision_rechaza_monto_invalido():
with pytest.raises(ValueError, match="mayor que cero"):
calcular_comision(0, "tarjeta")
def test_calcular_comision_transferencia_sin_comision():
assert calcular_comision(1000, "transferencia") == 0
def test_calcular_comision_rechaza_metodo_invalido():
with pytest.raises(ValueError, match="inválido"):
calcular_comision(1000, "efectivo")
Estas pruebas cubren validaciones y caminos alternativos sin depender de detalles internos.
Si prefieres compactar pruebas de autorización, puedes parametrizar los resultados normales:
import pytest
@pytest.mark.parametrize(
"monto, metodo, intentos, esperado",
[
(1000, "tarjeta", 0, "aprobado"),
(150000, "tarjeta", 0, "revision"),
(1000, "transferencia", 0, "pendiente"),
(1000, "efectivo", 0, "rechazado"),
(1000, "tarjeta", 3, "bloqueado"),
],
)
def test_autorizar_pago_resultados(monto, metodo, intentos, esperado):
assert autorizar_pago(monto, metodo, intentos) == esperado
Parametriza cuando los casos comparten intención. Mantén pruebas separadas si eso mejora la claridad.
Ejecuta otra vez:
python -m pytest --cov --cov-report=term-missing
El reporte de pagos.py debería mejorar notablemente:
Name Stmts Miss Branch BrPart Cover Missing
-------------------------------------------------------------------
src\tienda\pagos.py 24 0 12 0 100.00%
Si no llega a ese resultado, revisa qué líneas faltan y si representan comportamientos que todavía no fueron probados.
Para revisar visualmente el resultado:
python -m pytest --cov --cov-report=term-missing --cov-report=html
Abre htmlcov/index.html y revisa que los archivos críticos tengan cobertura suficiente y que no haya ramas parciales importantes.
Si configuraste fail_under = 80, la ejecución debería pasar cuando el total supere ese mínimo.
Si falla, no bajes el mínimo automáticamente. Primero revisa:
pyproject.toml.La cobertura de código es una herramienta de análisis. Ayuda a descubrir zonas sin ejecutar, ramas no probadas y módulos que necesitan más atención.
Usada con criterio, mejora la conversación sobre calidad. Usada como número aislado, puede llevar a pruebas débiles. La meta práctica es mantener pruebas que cubran comportamientos importantes y que sigan siendo útiles cuando el proyecto evoluciona.