¿Qué es la cobertura de caminos?
La cobertura de caminos requiere que cada camino único de ejecución a través de un programa o función se ejecute al menos una vez. Un camino es una secuencia completa de sentencias desde la entrada hasta la salida.
Considera una función con dos sentencias if secuenciales:
def process_order(amount, is_member):
discount = 0
if amount > 100: # Decisión 1
discount = 10
if is_member: # Decisión 2
discount += 5
return amount - discount
Cobertura de sentencias necesita tests que ejecuten cada línea — 2 tests podrían bastar.
Cobertura de decisiones necesita ambos True y False para cada decisión — 2 tests bastan.
Cobertura de caminos necesita cada combinación de caminos a través de todas las decisiones:
| Camino | Decisión 1 | Decisión 2 | Test Case |
|---|---|---|---|
| 1 | True | True | amount=200, is_member=True |
| 2 | True | False | amount=200, is_member=False |
| 3 | False | True | amount=50, is_member=True |
| 4 | False | False | amount=50, is_member=False |
Con 2 decisiones secuenciales, tenemos 4 caminos. Con N decisiones secuenciales, tenemos 2^N caminos. Este crecimiento exponencial es la razón por la que la cobertura completa de caminos es frecuentemente impráctica.
El problema de la explosión de caminos
Agrega un loop a la ecuación y los caminos se vuelven infinitos:
def retry_request(url, max_retries=3):
for attempt in range(max_retries):
response = send_request(url)
if response.status == 200:
return response
raise TimeoutError("Max retries exceeded")
El loop puede ejecutarse 0, 1, 2 o 3 veces. Cada iteración agrega un punto de decisión. El número de caminos distintos crece rápidamente. Para un loop que se ejecuta hasta N veces con M decisiones adentro, el total de caminos puede alcanzar M^N.
Por esto los profesionales usan basis path testing en lugar de cobertura exhaustiva de caminos.
Grafos de flujo de control
Antes de contar caminos, representa el código como un grafo de flujo de control (CFG):
- Nodos representan sentencias o bloques de sentencias secuenciales
- Aristas representan el flujo de control entre nodos
- Nodos de decisión tienen dos o más aristas salientes
Para la función process_order:
[Entry] → [discount=0] → <amount>100?>
→ Sí → [discount=10] → <is_member?>
→ No ─────────────────→ <is_member?>
→ Sí → [discount+=5] → [return] → [Exit]
→ No ──────────────→ [return] → [Exit]
Complejidad ciclomática
La complejidad ciclomática (introducida por Thomas McCabe en 1976) mide el número de caminos linealmente independientes a través de un programa. Provee el número mínimo de test cases necesarios para cobertura de basis path.
Tres fórmulas equivalentes:
- V(G) = E - N + 2 (E = aristas, N = nodos)
- V(G) = P + 1 (P = número de puntos de decisión/predicados)
- V(G) = R (R = número de regiones en el grafo planar, incluyendo la región exterior)
Guías prácticas de complejidad ciclomática
| Complejidad | Nivel de riesgo | Recomendación |
|---|---|---|
| 1-10 | Bajo | Simple, bajo riesgo — fácil de testear |
| 11-20 | Moderado | Más complejo — testing exhaustivo necesario |
| 21-50 | Alto | Complejo — considerar refactoring |
| 50+ | Muy alto | No testeable — debe refactorizarse |
La mayoría de los estándares de código recomiendan mantener la complejidad ciclomática por debajo de 10 por función. Si excede 10, la función hace demasiado.
Método de Basis Path Testing
El basis path testing de Tom McCabe proporciona una forma sistemática de derivar test cases:
Paso 1: Dibuja el grafo de flujo de control.
Paso 2: Calcula la complejidad ciclomática V(G).
Paso 3: Identifica el basis set de caminos independientes. Comienza con el camino más simple (típicamente el que atraviesa los branches más consistentemente), luego varía una decisión a la vez.
Paso 4: Crea test cases para cada basis path.
Ejemplo: Validación de login
def validate_login(username, password, is_locked):
if is_locked: # D1
return "ACCOUNT_LOCKED"
if not username: # D2
return "USERNAME_REQUIRED"
if len(password) < 8: # D3
return "PASSWORD_TOO_SHORT"
return "VALID"
V(G) = 3 + 1 = 4 (tres decisiones más uno).
Basis paths:
- D1=True → “ACCOUNT_LOCKED”
- D1=False, D2=True → “USERNAME_REQUIRED”
- D1=False, D2=False, D3=True → “PASSWORD_TOO_SHORT”
- D1=False, D2=False, D3=False → “VALID”
Test cases:
| # | username | password | is_locked | Esperado |
|---|---|---|---|---|
| 1 | “user” | “pass123” | True | ACCOUNT_LOCKED |
| 2 | "" | “pass123” | False | USERNAME_REQUIRED |
| 3 | “user” | “short” | False | PASSWORD_TOO_SHORT |
| 4 | “user” | “longpassword” | False | VALID |
Cuatro test cases logran cobertura de basis path.
Herramientas para análisis de caminos
- SonarQube — Reporta complejidad ciclomática por función y archivo
- Lizard — Analizador rápido de complejidad ciclomática soportando 20+ lenguajes
- radon — Análisis de complejidad específico para Python
- ESLint (regla complexity) — Impone complejidad ciclomática máxima para JavaScript/TypeScript
- PMD — Análisis de complejidad para Java/Apex
# Lizard: analizar complejidad de un proyecto
lizard src/ --CCN 10 --warnings_only
# radon: complejidad Python
radon cc my_module.py -a -nc
Ejercicio: Basis Path Testing
Problema 1
Deriva basis paths y test cases para esta función:
def calculate_shipping(weight, distance, is_express):
base_cost = 5.0
if weight > 10:
base_cost += weight * 0.5
else:
base_cost += weight * 0.3
if distance > 500:
base_cost *= 1.5
if is_express:
base_cost *= 2
return round(base_cost, 2)
Solución
Complejidad ciclomática: 3 decisiones → V(G) = 3 + 1 = 4
Nota: Decisión 1 es un if-else (2 branches), decisiones 2 y 3 son if simples (2 branches cada uno).
Basis paths:
- weight>10=T, distance>500=T, is_express=T — todos True
- weight>10=F, distance>500=T, is_express=T — variar D1
- weight>10=T, distance>500=F, is_express=T — variar D2
- weight>10=T, distance>500=T, is_express=F — variar D3
Test cases:
| # | weight | distance | is_express | Esperado |
|---|---|---|---|---|
| 1 | 15 | 600 | True | (5 + 15*0.5) * 1.5 * 2 = 37.5 |
| 2 | 5 | 600 | True | (5 + 5*0.3) * 1.5 * 2 = 19.5 |
| 3 | 15 | 200 | True | (5 + 15*0.5) * 1 * 2 = 25.0 |
| 4 | 15 | 600 | False | (5 + 15*0.5) * 1.5 * 1 = 18.75 |
Problema 2
Calcula la complejidad ciclomática y determina cuántos tests de basis path se necesitan:
def grade_student(score, attendance, has_extra_credit):
if score >= 90:
grade = "A"
elif score >= 80:
grade = "B"
elif score >= 70:
grade = "C"
else:
grade = "F"
if attendance < 75:
grade = "F"
if has_extra_credit and grade != "F":
grade = chr(ord(grade) - 1)
return grade
Solución
Contando decisiones:
- Cadena if/elif/elif/else = 3 puntos de decisión (3 condiciones evaluadas)
- Verificación de asistencia = 1 decisión
- Verificación de crédito extra = 1 decisión (compuesta, pero un nodo de decisión)
V(G) = 5 + 1 = 6
Necesitas 6 test cases de basis path como mínimo:
| # | score | attendance | extra_credit | Camino | Esperado |
|---|---|---|---|---|---|
| 1 | 95 | 80 | False | >=90, asistencia OK, sin extra | A |
| 2 | 85 | 80 | False | >=80, asistencia OK, sin extra | B |
| 3 | 75 | 80 | False | >=70, asistencia OK, sin extra | C |
| 4 | 50 | 80 | False | <70, asistencia OK, sin extra | F |
| 5 | 95 | 60 | False | >=90, asistencia BAJA, sin extra | F |
| 6 | 85 | 80 | True | >=80, asistencia OK, crédito extra | A |
Cobertura de caminos vs. otros criterios
| Criterio | Qué cubre | Tests mínimos | Subsume |
|---|---|---|---|
| Sentencias | Cada sentencia ejecutada | Menos | Nada |
| Decisiones | Cada branch tomado | Más | Sentencias |
| Condiciones | Cada condición T/F | Más | Nada por sí solo |
| MC/DC | Cada condición independiente | N+1 por decisión | Decisiones + Condiciones |
| Caminos | Cada camino único de ejecución | Más (exponencial) | Todos los anteriores |
La cobertura de caminos es el criterio más fuerte pero generalmente impráctica para cobertura completa. El basis path testing proporciona un compromiso práctico.
Cuándo usar cobertura de caminos
Usa basis path testing cuando:
- Testeas algoritmos críticos (cálculos financieros, lógica de seguridad)
- El código tiene complejidad moderada (V(G) < 15)
- Necesitas justificar la adecuación de tests ante auditores o revisores
- Refactorizas funciones complejas — los caminos te ayudan a entender qué hace el código
Omite cobertura completa de caminos cuando:
- Las funciones tienen loops con muchas iteraciones
- La complejidad ciclomática excede 20 (refactoriza primero)
- El código es suficientemente simple que la cobertura de decisiones basta
Puntos clave
- La cobertura de caminos testea cada camino de ejecución único desde entrada hasta salida
- La cobertura completa de caminos es impráctica debido al crecimiento exponencial y caminos infinitos de loops
- La complejidad ciclomática V(G) = P + 1 da el número mínimo de tests de basis path
- Basis path testing es el enfoque práctico — testea caminos independientes, no todos los caminos
- Mantén la complejidad ciclomática por debajo de 10 por función para código testeable y mantenible
- Usa herramientas como SonarQube, Lizard o radon para medir la complejidad automáticamente