¿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:

CaminoDecisión 1Decisión 2Test Case
1TrueTrueamount=200, is_member=True
2TrueFalseamount=200, is_member=False
3FalseTrueamount=50, is_member=True
4FalseFalseamount=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:

  1. V(G) = E - N + 2 (E = aristas, N = nodos)
  2. V(G) = P + 1 (P = número de puntos de decisión/predicados)
  3. 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

ComplejidadNivel de riesgoRecomendación
1-10BajoSimple, bajo riesgo — fácil de testear
11-20ModeradoMás complejo — testing exhaustivo necesario
21-50AltoComplejo — considerar refactoring
50+Muy altoNo 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:

  1. D1=True → “ACCOUNT_LOCKED”
  2. D1=False, D2=True → “USERNAME_REQUIRED”
  3. D1=False, D2=False, D3=True → “PASSWORD_TOO_SHORT”
  4. D1=False, D2=False, D3=False → “VALID”

Test cases:

#usernamepasswordis_lockedEsperado
1“user”“pass123”TrueACCOUNT_LOCKED
2""“pass123”FalseUSERNAME_REQUIRED
3“user”“short”FalsePASSWORD_TOO_SHORT
4“user”“longpassword”FalseVALID

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:

  1. weight>10=T, distance>500=T, is_express=T — todos True
  2. weight>10=F, distance>500=T, is_express=T — variar D1
  3. weight>10=T, distance>500=F, is_express=T — variar D2
  4. weight>10=T, distance>500=T, is_express=F — variar D3

Test cases:

#weightdistanceis_expressEsperado
115600True(5 + 15*0.5) * 1.5 * 2 = 37.5
25600True(5 + 5*0.3) * 1.5 * 2 = 19.5
315200True(5 + 15*0.5) * 1 * 2 = 25.0
415600False(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:

#scoreattendanceextra_creditCaminoEsperado
19580False>=90, asistencia OK, sin extraA
28580False>=80, asistencia OK, sin extraB
37580False>=70, asistencia OK, sin extraC
45080False<70, asistencia OK, sin extraF
59560False>=90, asistencia BAJA, sin extraF
68580True>=80, asistencia OK, crédito extraA

Cobertura de caminos vs. otros criterios

CriterioQué cubreTests mínimosSubsume
SentenciasCada sentencia ejecutadaMenosNada
DecisionesCada branch tomadoMásSentencias
CondicionesCada condición T/FMásNada por sí solo
MC/DCCada condición independienteN+1 por decisiónDecisiones + Condiciones
CaminosCada camino único de ejecuciónMá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