¿Qué es el data flow testing?
El data flow testing se enfoca en el ciclo de vida de las variables: dónde son definidas (se les asigna un valor), dónde son usadas (leídas) y dónde son eliminadas (salen de scope o son reasignadas). Al rastrear estos eventos a lo largo de los caminos de ejecución, el data flow testing revela defectos que otras técnicas no detectan.
Mientras el control flow testing pregunta “¿qué caminos toma el código?”, el data flow testing pregunta “¿qué pasa con los datos a lo largo de esos caminos?”
Estados de variables: Define, Use, Kill
Cada variable pasa por tres estados:
Define (d): La variable recibe un valor.
total = 0 # definición de total
user = get_user() # definición de user
Use (u): El valor de la variable es leído. Dos tipos:
- c-use (uso computacional): Valor usado en un cálculo:
result = total * tax_rate - p-use (uso en predicado): Valor usado en una condición:
if total > 100:
Kill (k): La variable deja de existir o es redefinida.
Anomalías de flujo de datos
Las anomalías son patrones sospechosos que frecuentemente indican bugs:
Anomalía dd (define-define)
Una variable es definida dos veces sin ser usada entre definiciones.
price = get_base_price() # define
price = get_sale_price() # define de nuevo — primera definición desperdiciada
discount = price * 0.1 # use
Anomalía ur (uso sin definición)
Una variable es usada antes de ser definida.
def calculate_total():
total = total + tax # BUG: total usada antes de definición
return total
Anomalía du (definición sin uso)
Una variable es definida pero nunca usada.
def process():
result = expensive_computation() # define
return "done" # result nunca usada
Pares Define-Use (pares DU)
Un par DU es un par (d, u) donde:
- d es una sentencia donde la variable v es definida
- u es una sentencia donde la variable v es usada
- Existe al menos un camino de d a u que no redefine v (un camino libre de definición)
Ejemplo
def process_payment(amount, discount_code):
price = amount # Línea 1: define price
if discount_code == "SAVE10": # Línea 2: use discount_code (p-use)
discount = 0.10 # Línea 3: define discount
elif discount_code == "SAVE20": # Línea 4: use discount_code (p-use)
discount = 0.20 # Línea 5: define discount
else:
discount = 0 # Línea 6: define discount
final = price * (1 - discount) # Línea 7: use price, use discount (c-use)
return final # Línea 8: use final (c-use)
Pares DU para discount: (3, 7), (5, 7), (6, 7)
Criterios de cobertura de flujo de datos
De más débil a más fuerte:
Cobertura All-Defs
Para cada definición de variable, al menos un par DU desde esa definición es cubierto.
Cobertura All-Uses
Para cada definición de variable, cada uso alcanzable es cubierto.
Cobertura All-DU-Paths
Para cada par DU, cada camino libre de definición entre la definición y el uso es cubierto.
Bugs comunes de flujo de datos
Null pointer por definición condicional:
if condition:
connection = create_connection()
# BUG: connection indefinida si condition es False
connection.execute(query)
Ejercicio: Análisis de flujo de datos
Problema 1
Identifica todos los pares DU y anomalías en esta función:
def calculate_grade(scores, curve):
total = 0 # Línea 1
count = 0 # Línea 2
average = 0 # Línea 3
for score in scores: # Línea 4
total = total + score # Línea 5
count = count + 1 # Línea 6
if count > 0: # Línea 7
average = total / count # Línea 8
average = average + curve # Línea 9
if average >= 90: # Línea 10
grade = "A" # Línea 11
elif average >= 80: # Línea 12
grade = "B" # Línea 13
else:
grade = "C" # Línea 14
return grade # Línea 15
Solución
Pares DU principales:
total: (1, 5-use), (5, 5-use), (5, 8)count: (2, 6-use), (6, 6-use), (6, 7), (6, 8)average: (3, 9) si count==0, (8, 9) si count>0grade: (11, 15), (13, 15), (14, 15)
Anomalía: Línea 3 define average = 0. Si scores está vacío, count=0, línea 8 se salta. Línea 9 usa el average = 0 inicial, resultando en grade = curve. Posible bug lógico.
Test cases para all-uses:
| # | scores | curve | Cubre |
|---|---|---|---|
| 1 | [85, 95] | 5 | Loop ejecuta, count>0, average calculado, >=90 |
| 2 | [70, 80] | 0 | Loop ejecuta, 80<=avg<90 |
| 3 | [50, 60] | 0 | Loop ejecuta, avg<80 |
| 4 | [] | 10 | Scores vacío, camino count=0 |
Problema 2
Encuentra y corrige bugs de flujo de datos:
def process_order(items, coupon):
subtotal = 0
shipping = 0
for item in items:
subtotal += item.price * item.quantity
if subtotal > 50:
shipping = 0
if coupon:
discount = subtotal * coupon.percent / 100
total = subtotal - discount + shipping
return total
Solución
Bug 1: anomalía ur — discount usada antes de definición. Si coupon es falsy, discount nunca se define. Fix: inicializar discount = 0.
Bug 2: anomalía dd — shipping siempre 0. Fix: agregar un valor de shipping para pedidos pequeños.
def process_order(items, coupon):
subtotal = 0
discount = 0
for item in items:
subtotal += item.price * item.quantity
if subtotal > 50:
shipping = 0
else:
shipping = 9.99
if coupon:
discount = subtotal * coupon.percent / 100
total = subtotal - discount + shipping
return total
Herramientas para análisis de flujo de datos
- SonarQube — Detecta código muerto, variables no usadas, riesgos de null pointer
- SpotBugs (Java) — Encuentra lecturas no inicializadas
- Pylint/Pyflakes (Python) — Reporta variables no usadas, nombres indefinidos
- ESLint (JavaScript) — Reglas no-unused-vars, no-undef
- Coverity — Herramienta comercial con análisis avanzado
Puntos clave
- El data flow testing rastrea variables a través del ciclo define → use → kill
- Los pares DU conectan definiciones con usos a lo largo de caminos libres de definición
- Tres niveles de cobertura: all-defs (más débil), all-uses, all-du-paths (más fuerte)
- Las anomalías (dd, ur, du) frecuentemente indican bugs reales
- Bug más común: variable usada en solo un branch de un condicional
- Las herramientas de análisis estático automatizan la detección de anomalías
- Aplica pensamiento de flujo de datos durante code review incluso sin herramientas formales