12. Diferencia entre cobertura de sentencias y calidad real de las pruebas

12.1 Objetivo del tema

La cobertura de sentencias indica qué líneas fueron ejecutadas por las pruebas. Eso es útil, pero no alcanza para afirmar que las pruebas sean buenas.

En este tema vamos a separar dos ideas: ejecutar código y verificar comportamiento. Una prueba puede cubrir muchas líneas y aun así detectar muy pocos errores.

Objetivo práctico: reconocer pruebas que suben la cobertura pero aportan poca confianza, y reemplazarlas por pruebas con aserciones significativas.

12.2 Cobertura no es calidad

Un reporte puede mostrar 100% de cobertura y aun así existir errores importantes. Coverage solo sabe si una sentencia se ejecutó; no sabe si la prueba comprobó correctamente el resultado.

Por eso la cobertura debe responder una pregunta acotada:

¿Qué partes del código fueron ejecutadas durante las pruebas?

La calidad de las pruebas requiere otra pregunta:

¿Las pruebas detectan errores reales en el comportamiento esperado?

12.3 Ejemplo de función con regla de negocio

Crea el archivo src/tienda/promociones.py:

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

    if cliente == "vip":
        return total * 0.20

    if cliente == "frecuente":
        return total * 0.10

    return 0

La función tiene tres resultados posibles y una validación. Es sencilla, pero alcanza para ver la diferencia entre cobertura y calidad.

12.4 Prueba débil con buena cobertura

Esta prueba ejecuta el camino de cliente vip, pero casi no verifica comportamiento:

from tienda.promociones import calcular_descuento


def test_calcular_descuento_vip_debil():
    resultado = calcular_descuento("vip", 1000)

    assert resultado is not None

La línea del descuento se ejecuta, entonces coverage la contará como cubierta. Pero si la función devolviera 50, 999 o "error", la prueba podría seguir pasando según el caso.

12.5 Prueba útil con aserción concreta

Una prueba más útil verifica la regla esperada:

def test_calcular_descuento_vip():
    assert calcular_descuento("vip", 1000) == 200

Ahora la prueba no solo ejecuta la línea. También confirma que el descuento para clientes VIP es del 20%.

12.6 Cobertura completa con pruebas incompletas

Podríamos cubrir todas las ramas con pruebas débiles:

import pytest


def test_descuento_vip_debil():
    assert calcular_descuento("vip", 1000) is not None


def test_descuento_frecuente_debil():
    assert calcular_descuento("frecuente", 1000) is not None


def test_descuento_comun_debil():
    assert calcular_descuento("comun", 1000) is not None


def test_descuento_total_invalido_debil():
    with pytest.raises(ValueError):
        calcular_descuento("vip", 0)

El reporte puede verse muy bien, pero las tres primeras pruebas no verifican los valores correctos. Ejecutan código, pero no protegen bien la regla de negocio.

12.7 Mejorar las aserciones

Una versión más confiable afirma resultados precisos:

import pytest


def test_descuento_vip():
    assert calcular_descuento("vip", 1000) == 200


def test_descuento_frecuente():
    assert calcular_descuento("frecuente", 1000) == 100


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


def test_descuento_rechaza_total_cero():
    with pytest.raises(ValueError, match="mayor que cero"):
        calcular_descuento("vip", 0)

La cobertura puede ser la misma, pero la calidad de las pruebas es mucho mayor porque cada prueba expresa una regla esperada.

12.8 Mutación mental

Una forma simple de evaluar una prueba es imaginar pequeños cambios incorrectos en el código. Por ejemplo:

if cliente == "vip":
    return total * 0.15

Una prueba débil con assert resultado is not None probablemente no detecte el error. Una prueba que espera 200 sí lo detecta.

Esta idea se parece al testing de mutación: introducir cambios pequeños y comprobar si las pruebas fallan. No necesitamos usar una herramienta todavía; basta con pensar si nuestras aserciones atraparían errores razonables.

12.9 Señales de pruebas débiles

  • Aserciones demasiado generales: is not None, len(resultado) > 0 o assert resultado sin verificar la regla real.
  • Pruebas sin aserciones: solo ejecutan código y pasan si no hay excepción.
  • Resultados no revisados: se llama a la función, pero no se comprueba lo que devuelve.
  • Excepciones demasiado amplias: se espera cualquier error en vez del error específico.

12.10 Pruebas sin aserciones

Esta prueba sube cobertura, pero no verifica nada concreto:

def test_descuento_sin_asercion():
    calcular_descuento("vip", 1000)

Solo fallaría si la función lanza una excepción inesperada. No detectaría un porcentaje mal calculado.

La corrección es agregar una aserción que represente la regla:

def test_descuento_vip_calcula_veinte_por_ciento():
    assert calcular_descuento("vip", 1000) == 200

12.11 Buen uso del reporte

El reporte de cobertura sigue siendo valioso. Ayuda a descubrir código que ninguna prueba ejecuta. Pero después de ubicar una línea faltante, hay que preguntarse:

  • Qué comportamiento representa esta línea.
  • Qué entrada permite llegar a ese comportamiento.
  • Qué resultado, excepción o cambio de estado debería verificarse.
  • Qué error real detectaría la prueba si el código cambia mal.

12.12 Cobertura como señal, no como meta única

Un porcentaje bajo suele indicar riesgo porque hay código sin ejecutar. Pero un porcentaje alto no garantiza que las pruebas sean suficientes.

La cobertura es más útil cuando se combina con otros criterios:

  • Relevancia: las pruebas cubren reglas importantes del negocio.
  • Precisión: las aserciones verifican resultados concretos.
  • Casos borde: se prueban límites y entradas inválidas.
  • Mantenibilidad: las pruebas son claras y no repiten detalles innecesarios.

12.13 Ejercicio práctico

Revisa una prueba existente de los temas anteriores y pregúntate si fallaría ante estos cambios:

  • Un porcentaje de descuento incorrecto.
  • Una validación eliminada.
  • Un caso borde tratado como caso normal.
  • Un método que ya no modifica el estado esperado.

Si la prueba seguiría pasando, probablemente necesita una aserción más específica.

12.14 Errores frecuentes

  • Perseguir 100% sin revisar aserciones: puedes cubrir líneas sin proteger comportamiento.
  • Creer que toda línea faltante exige una prueba: a veces el código sobra o debe rediseñarse.
  • Probar implementación en vez de comportamiento: evita pruebas atadas a detalles internos innecesarios.
  • Ignorar casos importantes porque la cobertura ya es alta: el riesgo del negocio importa más que el número.

12.15 Conclusión

En este tema vimos que cobertura de sentencias y calidad de pruebas no son lo mismo. La cobertura indica qué se ejecutó; las aserciones determinan qué comportamiento queda realmente protegido.

En el próximo tema vamos a avanzar hacia cobertura de ramas, donde ya no solo importa si una línea se ejecutó, sino qué caminos de decisión fueron recorridos.