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.
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:
La calidad de las pruebas requiere otra pregunta:
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.
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.
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%.
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.
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.
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.
is not None, len(resultado) > 0 o assert resultado sin verificar la regla real.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
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:
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:
Revisa una prueba existente de los temas anteriores y pregúntate si fallaría ante estos cambios:
Si la prueba seguiría pasando, probablemente necesita una aserción más específica.
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.