1. Qué es la cobertura de código y qué problemas ayuda a descubrir

1.1 Objetivo del tema

La cobertura de código mide qué partes de un programa fueron ejecutadas mientras corrían las pruebas. En Python podemos medirla para saber qué archivos, funciones, líneas y ramas fueron recorridos por la suite de pruebas.

En este primer tema veremos el concepto con un ejemplo pequeño. La idea no es instalar todavía todas las herramientas, sino entender qué información aporta la cobertura y cómo usarla para decidir qué pruebas faltan.

Objetivo práctico: leer código Python, identificar qué líneas ejecutan las pruebas y detectar qué comportamiento queda sin verificar.

1.2 Qué significa cubrir código

Decimos que una línea está cubierta cuando alguna prueba la ejecuta. Si una función tiene diez líneas y las pruebas ejecutan ocho, entonces hay dos líneas que no fueron recorridas durante la ejecución de la suite.

La cobertura no prueba por sí sola que el programa sea correcto. Solo indica que cierta parte del código fue ejecutada. Una prueba puede ejecutar una línea y, aun así, no verificar adecuadamente el resultado.

Cobertura alta no significa automáticamente buena calidad. Cobertura baja sí suele indicar zonas del sistema que merecen revisión.

1.3 Un módulo Python para analizar

Supongamos que tenemos un archivo llamado descuentos.py con una función que calcula descuentos según el tipo de cliente:

def calcular_descuento(total, tipo_cliente):
    if total <= 0:
        raise ValueError("El total debe ser mayor que cero")

    if tipo_cliente == "comun":
        return total * 0.05

    if tipo_cliente == "premium":
        return total * 0.15

    return 0

La función tiene varios comportamientos: valida datos inválidos, calcula descuento para clientes comunes, calcula descuento para clientes premium y devuelve cero para tipos de cliente sin descuento.

1.4 Una prueba que cubre solo una parte

Ahora escribimos una prueba en test_descuentos.py:

from descuentos import calcular_descuento


def test_descuento_cliente_comun():
    assert calcular_descuento(1000, "comun") == 50

Esta prueba ejecuta la función y verifica el caso de cliente común. Sin embargo, no ejecuta el caso de total inválido, cliente premium ni cliente sin descuento.

Si medimos cobertura, el reporte nos avisará que todavía hay líneas de descuentos.py que no fueron ejecutadas por las pruebas.

1.5 Qué problemas puede descubrir la cobertura

La cobertura ayuda a encontrar varios problemas frecuentes en una suite de pruebas:

  • Funciones sin pruebas: código que nunca se ejecuta durante la suite.
  • Casos borde olvidados: valores vacíos, negativos, nulos, extremos o inválidos.
  • Ramas no verificadas: caminos de if, elif, else, ciclos y excepciones.
  • Código muerto: partes del programa que parecen no usarse.
  • Falsa sensación de seguridad: muchas pruebas que pasan, pero que recorren siempre el mismo camino.

1.6 Mejorar las pruebas a partir del análisis

Al mirar el código anterior, podemos agregar pruebas para los comportamientos que faltan:

import pytest

from descuentos import calcular_descuento


def test_descuento_cliente_comun():
    assert calcular_descuento(1000, "comun") == 50


def test_descuento_cliente_premium():
    assert calcular_descuento(1000, "premium") == 150


def test_sin_descuento_para_cliente_no_registrado():
    assert calcular_descuento(1000, "invitado") == 0


def test_total_invalido_lanza_error():
    with pytest.raises(ValueError):
        calcular_descuento(0, "comun")

Estas pruebas recorren más caminos de la función. Además, verifican resultados concretos, que es lo importante: no solo ejecutan líneas, también comprueban el comportamiento esperado.

1.7 Cobertura de sentencias

La cobertura de sentencias mide si cada línea ejecutable fue recorrida al menos una vez. Es la forma más simple de cobertura y suele ser el primer reporte que analizamos.

En el ejemplo de calcular_descuento, una suite que prueba cliente común, cliente premium, cliente invitado y total inválido debería recorrer todas las sentencias de la función.

Una sentencia cubierta significa: "esta línea se ejecutó". No significa: "esta línea fue probada de manera completa".

1.8 Cobertura de ramas

La cobertura de ramas mira las decisiones del programa. Por ejemplo, en un if interesa saber si se probó el camino verdadero y también el camino falso.

Observa esta función:

def puede_comprar(edad, saldo):
    if edad >= 18 and saldo >= 100:
        return True
    return False

Una sola prueba con edad=20 y saldo=150 ejecuta la función, pero no analiza los casos donde la edad no alcanza, el saldo no alcanza o ambos datos son insuficientes. La cobertura de ramas ayuda a detectar esos caminos no revisados.

1.9 Qué no puede descubrir la cobertura

La cobertura es útil, pero tiene límites. No detecta automáticamente que una aserción sea débil, que el resultado esperado esté mal elegido o que falten escenarios importantes de negocio.

Por ejemplo, esta prueba ejecuta la función, pero no verifica nada útil:

def test_descuento_cliente_comun_debil():
    calcular_descuento(1000, "comun")

La línea queda cubierta, pero la prueba no comprueba el resultado. Por eso la cobertura debe usarse junto con buenas aserciones, casos representativos y criterio técnico.

1.10 Cómo interpretar un porcentaje

Un porcentaje de cobertura indica la proporción de código ejecutado por las pruebas. Si un archivo aparece con 60% de cobertura, significa que una parte importante no fue recorrida durante la suite.

El número sirve como señal inicial, pero la decisión importante está en las líneas faltantes. No todas las líneas tienen el mismo riesgo. Una condición de seguridad, una validación de pagos o un cálculo crítico merecen más atención que un bloque auxiliar de bajo impacto.

La pregunta correcta no es solo "cuánto cubrimos", sino "qué comportamiento importante todavía no estamos verificando".

1.11 Ejercicio práctico

Analiza el siguiente código y responde qué casos deberían probarse para cubrirlo mejor:

def clasificar_temperatura(grados):
    if grados < 0:
        return "bajo cero"

    if grados < 18:
        return "frio"

    if grados < 28:
        return "agradable"

    return "calor"

Una suite razonable debería incluir al menos un valor para cada resultado posible:

  • Un valor menor que 0.
  • Un valor entre 0 y 17.
  • Un valor entre 18 y 27.
  • Un valor igual o mayor que 28.

También conviene probar límites como 0, 18 y 28, porque los errores en comparaciones suelen aparecer justo en los bordes.

1.12 Pruebas posibles para el ejercicio

Una solución simple con pytest puede escribirse con parametrización:

import pytest

from temperaturas import clasificar_temperatura


@pytest.mark.parametrize(
    "grados, esperado",
    [
        (-5, "bajo cero"),
        (0, "frio"),
        (17, "frio"),
        (18, "agradable"),
        (27, "agradable"),
        (28, "calor"),
    ],
)
def test_clasificar_temperatura(grados, esperado):
    assert clasificar_temperatura(grados) == esperado
Pruebas parametrizadas para cubrir distintos resultados de una función

Estas pruebas recorren todos los resultados de la función y también revisan varios límites importantes. Más adelante mediremos esto con herramientas reales de cobertura.

1.13 Buen uso de la cobertura

La cobertura debe orientar el trabajo, no reemplazar el criterio del programador. Un buen proceso suele ser:

  • Ejecutar las pruebas.
  • Medir cobertura.
  • Revisar las líneas y ramas no cubiertas.
  • Decidir si falta una prueba, si el código es innecesario o si corresponde excluirlo con justificación.
  • Agregar pruebas que verifiquen comportamiento, no solo que ejecuten líneas.

1.14 Conclusión

La cobertura de código permite ver qué partes del programa fueron ejecutadas por las pruebas. Es especialmente útil para descubrir funciones sin probar, ramas olvidadas, validaciones no verificadas y zonas críticas con poca revisión.

También vimos una idea central para todo el curso: cubrir código no alcanza. Las pruebas deben tener aserciones claras y representar comportamientos importantes del sistema.

En el próximo tema prepararemos un proyecto Python específico para medir cobertura con herramientas reales.