La cobertura de código indica qué partes del programa fueron ejecutadas durante las pruebas. No garantiza que las pruebas sean buenas, pero ayuda a detectar funciones, ramas y líneas que todavía no tienen pruebas.
En este tema mediremos cobertura usando coverage.py y pytest-cov.
Crea un proyecto nuevo:
mkdir pytest-cobertura-demo
cd pytest-cobertura-demo
Instala las herramientas necesarias:
python -m pip install pytest coverage pytest-cov
coverage.py es la herramienta que mide cobertura. pytest-cov es un complemento que integra esa medición con pytest.
coverage: permite ejecutar pruebas y generar reportes.pytest-cov: permite usar opciones como --cov dentro de pytest.Crea un archivo llamado descuentos.py:
def calcular_descuento(precio, porcentaje):
if precio < 0:
raise ValueError("El precio no puede ser negativo")
if porcentaje < 0 or porcentaje > 100:
raise ValueError("El porcentaje debe estar entre 0 y 100")
return precio - (precio * porcentaje / 100)
def clasificar_cliente(total_compras):
if total_compras >= 100000:
return "premium"
if total_compras >= 30000:
return "frecuente"
return "nuevo"
def aplicar_beneficio(precio, tipo_cliente):
if tipo_cliente == "premium":
return calcular_descuento(precio, 20)
if tipo_cliente == "frecuente":
return calcular_descuento(precio, 10)
return precio
El módulo tiene validaciones y ramas. Eso lo vuelve útil para practicar cobertura.
Crea test_descuentos.py con algunas pruebas:
from descuentos import aplicar_beneficio, calcular_descuento, clasificar_cliente
def test_calcular_descuento():
assert calcular_descuento(1000, 10) == 900
def test_clasificar_cliente_premium():
assert clasificar_cliente(120000) == "premium"
def test_aplicar_beneficio_premium():
assert aplicar_beneficio(1000, "premium") == 800
Estas pruebas pasan, pero no cubren todos los caminos del código.
Ejecuta:
python -m pytest --cov=descuentos
La salida mostrará un resumen de cobertura similar a:
Name Stmts Miss Cover
-----------------------------------
descuentos.py 16 6 62%
-----------------------------------
TOTAL 16 6 62%
Los números exactos pueden variar si modificas el código.
Para saber qué líneas no fueron ejecutadas, agrega --cov-report=term-missing:
python -m pytest --cov=descuentos --cov-report=term-missing
La columna Missing muestra líneas sin cubrir:
Name Stmts Miss Cover Missing
---------------------------------------------
descuentos.py 16 6 62% 3, 5, 11-12, 19-20
Este reporte indica dónde conviene revisar si faltan pruebas.
Agrega pruebas para los errores:
import pytest
def test_calcular_descuento_con_precio_negativo():
with pytest.raises(ValueError):
calcular_descuento(-100, 10)
def test_calcular_descuento_con_porcentaje_invalido():
with pytest.raises(ValueError):
calcular_descuento(1000, 150)
Estas pruebas cubren ramas que antes no se ejecutaban.
La función clasificar_cliente tiene tres caminos:
def test_clasificar_cliente_frecuente():
assert clasificar_cliente(50000) == "frecuente"
def test_clasificar_cliente_nuevo():
assert clasificar_cliente(10000) == "nuevo"
Si una función tiene varias ramas, conviene cubrir al menos un caso representativo de cada una.
También faltan los caminos de clientes frecuentes y nuevos:
def test_aplicar_beneficio_frecuente():
assert aplicar_beneficio(1000, "frecuente") == 900
def test_aplicar_beneficio_nuevo():
assert aplicar_beneficio(1000, "nuevo") == 1000
El archivo test_descuentos.py puede quedar así:
import pytest
from descuentos import aplicar_beneficio, calcular_descuento, clasificar_cliente
def test_calcular_descuento():
assert calcular_descuento(1000, 10) == 900
def test_calcular_descuento_con_precio_negativo():
with pytest.raises(ValueError):
calcular_descuento(-100, 10)
def test_calcular_descuento_con_porcentaje_invalido():
with pytest.raises(ValueError):
calcular_descuento(1000, 150)
def test_clasificar_cliente_premium():
assert clasificar_cliente(120000) == "premium"
def test_clasificar_cliente_frecuente():
assert clasificar_cliente(50000) == "frecuente"
def test_clasificar_cliente_nuevo():
assert clasificar_cliente(10000) == "nuevo"
def test_aplicar_beneficio_premium():
assert aplicar_beneficio(1000, "premium") == 800
def test_aplicar_beneficio_frecuente():
assert aplicar_beneficio(1000, "frecuente") == 900
def test_aplicar_beneficio_nuevo():
assert aplicar_beneficio(1000, "nuevo") == 1000
Ejecuta otra vez:
python -m pytest --cov=descuentos --cov-report=term-missing
La salida esperada será similar a:
Name Stmts Miss Cover Missing
---------------------------------------------
descuentos.py 16 0 100%
---------------------------------------------
TOTAL 16 0 100%
En este ejemplo pequeño llegamos a 100%, pero en proyectos reales no siempre será necesario ni razonable.
Para crear un reporte navegable:
python -m pytest --cov=descuentos --cov-report=html
Se creará una carpeta llamada htmlcov. Abre este archivo en el navegador:
htmlcov\index.html
El reporte HTML marca líneas ejecutadas, líneas faltantes y archivos incluidos.
También puedes usar coverage sin pytest-cov:
python -m coverage run -m pytest
python -m coverage report -m
python -m coverage html
Esta forma es útil cuando quieres separar ejecución y generación de reportes.
En un proyecto con carpeta src, puedes medir esa carpeta completa:
python -m pytest --cov=src --cov-report=term-missing
En un proyecto con paquete llamado tienda:
python -m pytest --cov=tienda --cov-report=term-missing
Para fallar la suite si la cobertura baja de un porcentaje:
python -m pytest --cov=descuentos --cov-fail-under=80
Esto es útil en proyectos compartidos, pero debe elegirse con criterio. Un umbral demasiado alto puede incentivar pruebas superficiales.
Podemos guardar opciones comunes en pytest.ini:
[pytest]
addopts = --cov=descuentos --cov-report=term-missing --cov-fail-under=80
Con esa configuración, alcanza con ejecutar:
python -m pytest
Algunos archivos no suelen formar parte de la cobertura, como pruebas, archivos de configuración o scripts temporales. Con coverage puedes crear .coveragerc:
[run]
omit =
tests/*
*/__init__.py
scripts/*
No omitas código solo para mejorar el porcentaje. Omite archivos que realmente no representan lógica del producto.
Una línea cubierta solo significa que se ejecutó. No significa que tenga una aserción útil ni que todos los casos importantes estén probados.
def test_mala_prueba():
calcular_descuento(1000, 10)
Esta prueba ejecuta código y aumenta cobertura, pero no verifica el resultado. Una prueba útil necesita aserciones.
--cov apuntando al paquete o módulo correcto.mkdir pytest-cobertura-demo
cd pytest-cobertura-demo
python -m pip install pytest coverage pytest-cov
python -m pytest
python -m pytest --cov=descuentos
python -m pytest --cov=descuentos --cov-report=term-missing
python -m pytest --cov=descuentos --cov-report=html
python -m pytest --cov=descuentos --cov-fail-under=80
python -m coverage run -m pytest
python -m coverage report -m
python -m coverage html
coverage.py mide qué código se ejecutó durante las pruebas.pytest-cov integra cobertura con pytest.--cov-report=term-missing muestra líneas sin cubrir.--cov-report=html genera un reporte navegable.--cov-fail-under exige un mínimo de cobertura.En este tema medimos cobertura con pytest-cov y coverage.py, revisamos líneas faltantes, generamos reportes HTML y configuramos un umbral mínimo.
En el próximo tema veremos cómo configurar el proyecto con pyproject.toml o pytest.ini.