De código a grafos

El control flow testing usa la estructura del código — sus branches, loops y secuencias — como base para el diseño de tests. La herramienta principal es el grafo de flujo de control (CFG), que provee una representación visual de todos los caminos de ejecución posibles.

A diferencia de las técnicas black-box que ignoran la implementación, el control flow testing es una técnica white-box que requiere acceso al código fuente.

Construyendo un grafo de flujo de control

Elementos básicos

Cada CFG consiste en:

  • Nodo de inicio: Punto de entrada de la función
  • Nodo de fin: Punto(s) de salida
  • Nodos de proceso: Sentencias secuenciales
  • Nodos de decisión: Puntos de ramificación — if, switch, ternario
  • Nodos de unión: Donde los branches se juntan
  • Aristas: Flechas dirigidas mostrando el flujo

Sentencias secuenciales

Sentencias que siempre se ejecutan juntas se colapsan en un solo nodo:

a = 1
b = 2
c = a + b

Estructuras If-Else

if condition:
    do_something()
else:
    do_other()
result = finish()

Loops

while condition:
    process()
post_loop()

La back edge del cuerpo del loop al nodo de condición crea un ciclo en el grafo.

Sentencias Switch/Case

Un switch crea un nodo de decisión con múltiples aristas salientes — una por case.

Estrategias de testing de loops

Los loops son las estructuras más propensas a errores. Un enfoque sistemático testea los loops en sus boundaries:

Testing de loop simple (Loop Testing de Beizer)

Para un loop que itera de 0 a N veces:

TestIteracionesPor qué
10 (saltar loop)Testea la condición de guarda con falso inmediato
21Ejecución mínima — atrapa errores de inicialización
32Testea transición de primera a segunda iteración
4N-1Justo bajo el máximo
5NMáximo de iteraciones — testea condición de terminación
6N+1 (si posible)Más allá del máximo — testea overflow

Testing de loops anidados

  1. Comienza con el loop más interno. Fija loops externos al mínimo. Testea el loop interno con testing simple.
  2. Muévete hacia afuera un nivel a la vez.
  3. Testea interacciones ejecutando el loop externo con el interno en valores boundary.

Loops concatenados

Dos loops en secuencia: testea cada uno independientemente a menos que el segundo use datos del primero.

Dominadores y post-dominadores

Dominador: El nodo A domina al nodo B si cada camino desde la entrada a B pasa por A.

Post-dominador: El nodo A post-domina a B si cada camino desde B a la salida pasa por A.

Estos conceptos ayudan a identificar:

  • Encabezados de loop — nodos que dominan la fuente de la back edge
  • Loops naturales — definidos por back edges
  • Aristas críticas — cuya eliminación desconecta partes del grafo

Construcción práctica de CFG

  1. Numera cada sentencia o agrupa sentencias secuenciales en bloques
  2. Marca puntos de decisión (if, while, for, switch, try/catch)
  3. Dibuja aristas de cada bloque a sus sucesores
  4. Marca back edges para loops
  5. Verifica: La entrada debe alcanzar cada nodo; cada nodo debe alcanzar la salida

Ejemplo: Procesamiento de orden

def process_order(order):
    if not order.items:
        return {"error": "Empty order"}

    total = 0
    for item in order.items:
        if item.quantity <= 0:
            continue
        total += item.price * item.quantity

    if total > 0:
        tax = total * 0.1
        return {"total": total + tax}
    else:
        return {"error": "Invalid total"}

Ejercicio: Construir y testear CFGs

Problema 1

Dibuja el CFG y deriva test cases:

def authenticate(username, password, two_factor_code):
    user = find_user(username)

    if user is None:
        return "USER_NOT_FOUND"

    if not verify_password(user, password):
        user.failed_attempts += 1
        if user.failed_attempts >= 3:
            lock_account(user)
            return "ACCOUNT_LOCKED"
        return "WRONG_PASSWORD"

    if user.requires_2fa:
        if not verify_2fa(user, two_factor_code):
            return "INVALID_2FA"

    create_session(user)
    return "SUCCESS"
Solución

Complejidad ciclomática: 4 decisiones + 1 = 5

Test cases (basis paths):

#usernamepassword2fa_codeCaminoEsperado
1“unknown”anyanyuser is NoneUSER_NOT_FOUND
2“valid”“wrong”anypassword fail, attempts < 3WRONG_PASSWORD
3“valid”“wrong”anypassword fail, attempts >= 3ACCOUNT_LOCKED
4“valid_2fa”“correct”“wrong”2FA failINVALID_2FA
5“valid_2fa”“correct”“valid”Todo OK con 2FASUCCESS
6“valid_no2fa”“correct”anySin 2FASUCCESS

Problema 2

Aplica testing de loops:

def find_duplicates(items, max_scan=100):
    seen = set()
    duplicates = []

    for i, item in enumerate(items):
        if i >= max_scan:
            break
        if item in seen:
            duplicates.append(item)
        else:
            seen.add(item)

    return duplicates
Solución
#itemsmax_scanIteracionesTestea
1[]1000Lista vacía
2[“a”]1001Un item
3[“a”, “a”]1002Duplicado en segunda iteración
4[“a”,“b”,“c”,“d”,“e”]1005Sin duplicados
5[“a”,“b”,“a”,“c”,“b”]1005Con duplicados
6[“a”,“b”,“c”]22max_scan alcanzado
7[“a”,“b”,“c”]00max_scan=0

Análisis CFG en la práctica

En proyectos reales, rara vez dibujas CFGs a mano para cada función:

  1. Usa herramientas. Muchos IDEs visualizan flujo de control.
  2. Enfócate en funciones complejas. V(G) > 10 se beneficia más del análisis CFG.
  3. Busca patrones — if-else profundamente anidados, loops con múltiples salidas.
  4. Simplifica primero. Si un CFG es demasiado complejo, la función necesita refactoring.

Puntos clave

  • Los grafos de flujo de control representan todos los caminos posibles a través del código
  • Nodos son bloques de sentencias; aristas son flujo entre ellos; back edges crean loops
  • Testing de loops sigue la estrategia de Beizer: 0, 1, 2, N-1, N, N+1 iteraciones
  • Loops anidados: testea loops internos primero, luego trabaja hacia afuera
  • Los dominadores identifican encabezados de loops y relaciones estructurales críticas
  • La complejidad ciclomática del CFG da el número mínimo de tests de basis path
  • Usa análisis CFG principalmente para funciones complejas (V(G) > 10)