Muchas líneas faltantes en un reporte de cobertura corresponden a validaciones, excepciones y casos borde. Es normal que el camino principal esté probado primero y que los caminos de error queden pendientes.
En este tema vamos a transformar esas líneas faltantes en pruebas claras, usando pytest.raises para excepciones y parametrización para límites.
Crea el archivo src/tienda/cupones.py:
def aplicar_cupon(total, codigo):
if total <= 0:
raise ValueError("El total debe ser mayor que cero")
if not codigo:
return total
codigo = codigo.upper()
if codigo == "DESC10":
return total * 0.90
if codigo == "DESC20":
return total * 0.80
raise ValueError("Cupón inválido")
def calcular_puntos(total):
if total < 0:
raise ValueError("El total no puede ser negativo")
if total < 1000:
return 0
if total < 10000:
return int(total // 1000)
return int(total // 500)
El módulo mezcla caminos normales, ausencia de cupón, cupones válidos, cupón inválido y límites para cálculo de puntos.
Una primera versión de pruebas podría cubrir solo el camino principal:
from tienda.cupones import aplicar_cupon, calcular_puntos
def test_aplicar_cupon_desc10():
assert aplicar_cupon(1000, "DESC10") == 900
def test_calcular_puntos_compra_media():
assert calcular_puntos(3000) == 3
Estas pruebas son válidas, pero dejan sin ejecutar varios caminos: total inválido, ausencia de cupón, otro cupón válido, cupón inválido y límites del cálculo de puntos.
Ejecuta el reporte con líneas faltantes.
En Windows PowerShell:
$env:PYTHONPATH="src"
python -m pytest --cov=src --cov-report=term-missing
En Linux o macOS:
PYTHONPATH=src python -m pytest --cov=src --cov-report=term-missing
Una salida posible para cupones.py sería:
Name Stmts Miss Cover Missing
-----------------------------------------------------
src\tienda\cupones.py 22 9 59% 3, 6, 14, 16, 21, 24, 27, 29, 31
Los números pueden variar, pero el patrón es importante: el reporte apunta a caminos que todavía no tienen pruebas.
Para probar excepciones, no alcanza con ejecutar la función. Hay que verificar que lance el error esperado.
import pytest
from tienda.cupones import aplicar_cupon, calcular_puntos
def test_aplicar_cupon_rechaza_total_cero():
with pytest.raises(ValueError):
aplicar_cupon(0, "DESC10")
def test_aplicar_cupon_rechaza_codigo_invalido():
with pytest.raises(ValueError):
aplicar_cupon(1000, "NOEXISTE")
def test_calcular_puntos_rechaza_total_negativo():
with pytest.raises(ValueError):
calcular_puntos(-1)
Estas pruebas cubren caminos de error y documentan qué entradas no son aceptadas.
Cuando el mensaje forma parte del comportamiento esperado, también se puede verificar:
def test_aplicar_cupon_rechaza_codigo_invalido_con_mensaje():
with pytest.raises(ValueError, match="Cupón inválido"):
aplicar_cupon(1000, "NOEXISTE")
No conviene verificar mensajes si cambian con frecuencia o si son solo detalles internos. Sí conviene hacerlo cuando ayudan a asegurar una regla de negocio o una respuesta esperada.
El camino if not codigo representa una decisión válida: comprar sin cupón debe devolver el total sin descuento.
def test_aplicar_cupon_sin_codigo_devuelve_total_original():
assert aplicar_cupon(1000, "") == 1000
Esta prueba no se escribe solo porque falta una línea. Se escribe porque comprar sin cupón es un comportamiento real.
Los casos borde aparecen alrededor de los valores donde cambia la regla. En calcular_puntos, los límites importantes son 0, 1000 y 10000.
def test_calcular_puntos_menor_a_mil():
assert calcular_puntos(999) == 0
def test_calcular_puntos_en_mil():
assert calcular_puntos(1000) == 1
def test_calcular_puntos_debajo_de_diez_mil():
assert calcular_puntos(9999) == 9
def test_calcular_puntos_en_diez_mil():
assert calcular_puntos(10000) == 20
Estas pruebas revisan los puntos donde suele haber errores de comparación: <, <=, > y >=.
Cuando varias pruebas tienen la misma estructura, conviene parametrizar:
import pytest
@pytest.mark.parametrize(
"total, esperado",
[
(0, 0),
(999, 0),
(1000, 1),
(9999, 9),
(10000, 20),
],
)
def test_calcular_puntos_casos_borde(total, esperado):
assert calcular_puntos(total) == esperado
La parametrización permite mantener visible la tabla de casos sin repetir el mismo cuerpo de prueba muchas veces.
Si existen dos cupones válidos, ambos deben estar representados en las pruebas:
@pytest.mark.parametrize(
"codigo, esperado",
[
("DESC10", 900),
("DESC20", 800),
("desc10", 900),
],
)
def test_aplicar_cupon_codigos_validos(codigo, esperado):
assert aplicar_cupon(1000, codigo) == esperado
El caso "desc10" también comprueba que la función acepta minúsculas porque convierte el código con upper().
Después de agregar las pruebas, vuelve a ejecutar:
En Windows PowerShell:
$env:PYTHONPATH="src"
python -m pytest --cov=src --cov-report=term-missing
En Linux o macOS:
PYTHONPATH=src python -m pytest --cov=src --cov-report=term-missing
La cobertura de cupones.py debería subir y la lista de líneas faltantes debería reducirse. Si todavía quedan líneas sin cubrir, revisa si representan un comportamiento necesario o código que conviene simplificar.
pytest.raises para confirmar que el error esperado ocurre.En este tema usamos cobertura para detectar validaciones, excepciones y casos borde sin probar. Luego agregamos pruebas con pytest.raises y parametrización para cubrir reglas importantes.
En el próximo tema vamos a trabajar con clases, métodos y cambios de estado, donde la cobertura requiere mirar no solo entradas y salidas, sino también cómo evoluciona el objeto.