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:
| Test | Iteraciones | Por qué |
|---|---|---|
| 1 | 0 (saltar loop) | Testea la condición de guarda con falso inmediato |
| 2 | 1 | Ejecución mínima — atrapa errores de inicialización |
| 3 | 2 | Testea transición de primera a segunda iteración |
| 4 | N-1 | Justo bajo el máximo |
| 5 | N | Máximo de iteraciones — testea condición de terminación |
| 6 | N+1 (si posible) | Más allá del máximo — testea overflow |
Testing de loops anidados
- Comienza con el loop más interno. Fija loops externos al mínimo. Testea el loop interno con testing simple.
- Muévete hacia afuera un nivel a la vez.
- 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
- Numera cada sentencia o agrupa sentencias secuenciales en bloques
- Marca puntos de decisión (if, while, for, switch, try/catch)
- Dibuja aristas de cada bloque a sus sucesores
- Marca back edges para loops
- 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):
| # | username | password | 2fa_code | Camino | Esperado |
|---|---|---|---|---|---|
| 1 | “unknown” | any | any | user is None | USER_NOT_FOUND |
| 2 | “valid” | “wrong” | any | password fail, attempts < 3 | WRONG_PASSWORD |
| 3 | “valid” | “wrong” | any | password fail, attempts >= 3 | ACCOUNT_LOCKED |
| 4 | “valid_2fa” | “correct” | “wrong” | 2FA fail | INVALID_2FA |
| 5 | “valid_2fa” | “correct” | “valid” | Todo OK con 2FA | SUCCESS |
| 6 | “valid_no2fa” | “correct” | any | Sin 2FA | SUCCESS |
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
| # | items | max_scan | Iteraciones | Testea |
|---|---|---|---|---|
| 1 | [] | 100 | 0 | Lista vacía |
| 2 | [“a”] | 100 | 1 | Un item |
| 3 | [“a”, “a”] | 100 | 2 | Duplicado en segunda iteración |
| 4 | [“a”,“b”,“c”,“d”,“e”] | 100 | 5 | Sin duplicados |
| 5 | [“a”,“b”,“a”,“c”,“b”] | 100 | 5 | Con duplicados |
| 6 | [“a”,“b”,“c”] | 2 | 2 | max_scan alcanzado |
| 7 | [“a”,“b”,“c”] | 0 | 0 | max_scan=0 |
Análisis CFG en la práctica
En proyectos reales, rara vez dibujas CFGs a mano para cada función:
- Usa herramientas. Muchos IDEs visualizan flujo de control.
- Enfócate en funciones complejas. V(G) > 10 se beneficia más del análisis CFG.
- Busca patrones — if-else profundamente anidados, loops con múltiples salidas.
- 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)