En el tema anterior vimos que una línea cubierta no siempre significa que todos sus caminos fueron probados. Ahora vamos a activar la cobertura de ramas para que coverage.py detecte esas decisiones parcialmente cubiertas.
La cobertura de ramas ayuda a encontrar caminos faltantes en if, else, elif, ciclos, retornos tempranos y condiciones compuestas.
--branch y leer columnas como Branch y BrPart.
Crea el archivo src/tienda/riesgo.py:
def evaluar_riesgo(total, intentos_fallidos, cliente_vip=False):
if total <= 0:
raise ValueError("El total debe ser mayor que cero")
if cliente_vip:
return "bajo"
if intentos_fallidos >= 3:
return "alto"
if total >= 100000:
return "medio"
return "bajo"
La función tiene varias decisiones. Algunas pruebas pueden cubrir líneas sin recorrer todos los caminos.
Crea tests/test_riesgo.py con estas pruebas:
import pytest
from tienda.riesgo import evaluar_riesgo
def test_evaluar_riesgo_cliente_vip():
assert evaluar_riesgo(50000, 0, cliente_vip=True) == "bajo"
def test_evaluar_riesgo_intentos_fallidos():
assert evaluar_riesgo(50000, 3) == "alto"
def test_evaluar_riesgo_rechaza_total_cero():
with pytest.raises(ValueError):
evaluar_riesgo(0, 0)
Estas pruebas son útiles, pero todavía falta el camino de total alto para cliente no VIP y el camino final de riesgo bajo.
Con coverage.py, agrega la opción --branch al ejecutar la medición.
En Windows PowerShell:
$env:PYTHONPATH="src"
python -m coverage run --branch -m pytest
python -m coverage report -m
En Linux o macOS:
PYTHONPATH=src python -m coverage run --branch -m pytest
python -m coverage report -m
El reporte agrega información de ramas:
Name Stmts Miss Branch BrPart Cover Missing
-----------------------------------------------------------------
src\tienda\riesgo.py 11 2 8 1 79% 11, 13
Si usas pytest-cov, agrega --cov-branch:
En Windows PowerShell:
$env:PYTHONPATH="src"
python -m pytest --cov=src --cov-branch --cov-report=term-missing
En Linux o macOS:
PYTHONPATH=src python -m pytest --cov=src --cov-branch --cov-report=term-missing
El resultado es equivalente: pytest ejecuta las pruebas y coverage mide también las ramas.
Branch indica la cantidad de ramas detectadas en el archivo. No son líneas, sino caminos de decisión.
Por ejemplo, cada if puede aportar caminos de salida: condición verdadera y condición falsa. Coverage cuenta esas posibilidades para evaluar si fueron recorridas.
Un archivo puede tener buena cobertura de sentencias y menor cobertura de ramas si ejecutó las líneas pero dejó caminos sin recorrer.
BrPart significa branches partial, es decir, ramas parcialmente cubiertas.
Una rama parcial aparece cuando coverage vio una decisión, pero no todos sus caminos fueron ejecutados. Por ejemplo:
if total >= 100000:
return "medio"
return "bajo"
Si solo probamos totales menores a 100000, el if se evalúa, pero nunca toma el camino verdadero.
Para cubrir los caminos pendientes de riesgo.py, agregamos pruebas para total alto y para riesgo bajo final:
def test_evaluar_riesgo_total_alto():
assert evaluar_riesgo(100000, 0) == "medio"
def test_evaluar_riesgo_bajo_sin_condiciones_especiales():
assert evaluar_riesgo(50000, 0) == "bajo"
Estas pruebas no solo reducen BrPart. También documentan dos reglas de negocio que no estaban verificadas.
Vuelve a ejecutar la cobertura de ramas:
En Windows PowerShell:
$env:PYTHONPATH="src"
python -m pytest --cov=src --cov-branch --cov-report=term-missing
En Linux o macOS:
PYTHONPATH=src python -m pytest --cov=src --cov-branch --cov-report=term-missing
El valor de BrPart debería bajar o desaparecer para el archivo analizado.
El reporte HTML es especialmente útil para branch coverage:
En Windows PowerShell:
$env:PYTHONPATH="src"
python -m pytest --cov=src --cov-branch --cov-report=html
En Linux o macOS:
PYTHONPATH=src python -m pytest --cov=src --cov-branch --cov-report=html
Luego abre htmlcov/index.html. En los archivos con ramas parciales, el reporte visual ayuda a ubicar qué decisión quedó incompleta.
Algunas ramas parciales pueden corresponder a código defensivo, condiciones imposibles por diseño o caminos que conviene excluir con una justificación clara.
Antes de agregar una prueba, pregúntate:
Las condiciones con and y or merecen atención especial:
def requiere_revision(total, intentos_fallidos):
return total >= 100000 or intentos_fallidos >= 3
Casos útiles:
def test_requiere_revision_por_total():
assert requiere_revision(100000, 0) is True
def test_requiere_revision_por_intentos():
assert requiere_revision(1000, 3) is True
def test_no_requiere_revision():
assert requiere_revision(1000, 0) is False
Estas pruebas cubren las razones principales por las que la función puede devolver True y el camino donde devuelve False.
En este tema activamos cobertura de ramas con --branch y --cov-branch. También interpretamos Branch, BrPart y los reportes de ramas parciales.
En el próximo tema vamos a usar parametrización para aumentar cobertura sin duplicar código.