30. Medir cobertura con coverage.py y pytest-cov

30.1 Objetivo del tema

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.

Idea clave: la cobertura no mide calidad; mide qué código fue recorrido por las pruebas.

30.2 Crear una carpeta de práctica

Crea un proyecto nuevo:

mkdir pytest-cobertura-demo
cd pytest-cobertura-demo

Instala las herramientas necesarias:

python -m pip install pytest coverage pytest-cov

30.3 coverage.py y 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.

30.4 Crear el módulo a probar

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.

30.5 Crear pruebas iniciales

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.

30.6 Ejecutar pruebas con cobertura usando pytest-cov

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.

30.7 Mostrar líneas faltantes

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.

30.8 Agregar pruebas para validaciones

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.

30.9 Agregar pruebas para clasificación

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.

30.10 Agregar pruebas para beneficios

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

30.11 Archivo completo de pruebas

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

30.12 Ejecutar nuevamente con cobertura

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.

30.13 Generar reporte HTML

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.

30.14 Usar coverage.py directamente

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.

30.15 Medir cobertura de una carpeta

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

30.16 Exigir un mínimo de cobertura

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.

30.17 Configurar cobertura en pytest.ini

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

30.18 Omitir archivos del reporte

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.

30.19 Interpretar cobertura con criterio

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.

30.20 Errores frecuentes

  • Confundir cobertura con calidad: una cobertura alta no garantiza buenas pruebas.
  • Medir archivos equivocados: usa --cov apuntando al paquete o módulo correcto.
  • No mirar líneas faltantes: el porcentaje solo no alcanza para decidir qué probar.
  • Subir el porcentaje con pruebas sin assert: eso da una falsa sensación de seguridad.
  • Configurar un umbral irreal: el objetivo debe ayudar, no bloquear trabajo razonable.

30.21 Comandos usados en este tema

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

30.22 Qué debes recordar de este tema

  • 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.
  • La cobertura debe interpretarse junto con la calidad de las aserciones.

30.23 Conclusión

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.