El Problema de los Tests Flaky
Los tests flaky son la plaga de la automatización de pruebas moderna. Pasan y fallan intermitentemente sin cambios en el código, erosionando la confianza en los test suites y desperdiciando horas de ingeniería en investigación. Los estudios muestran que 15-25% de los tests en bases de código grandes exhiben comportamiento flaky, y los equipos gastan 10-30% del tiempo de QA debuggeando fallos falsos.
Los enfoques tradicionales para detectar tests flaky dependen de re-ejecutar tests múltiples veces y analizar patrones manualmente. Esto es lento, costoso y reactivo. Machine Learning (como se discute en AI Code Smell Detection: Finding Problems in Test Automation with ML) ofrece una solución proactiva: predecir qué tests probablemente serán flaky antes de que causen problemas, identificar causas raíz automáticamente y sugerir correcciones.
Este artículo explora cómo aprovechar ML (como se discute en AI-powered Test Generation: The Future Is Already Here) para detección de tests flaky, con algoritmos prácticos, ejemplos de implementación y estrategias probadas para construir test suites estables.
Entendiendo los Tests Flaky
Tipos de Flakiness
Tipo | Descripción | Ejemplo |
---|---|---|
Dependiente del Orden | El resultado del test depende del orden de ejecución | Test A pasa solo pero falla después del Test B |
Problemas de Espera Async | Race conditions, esperas insuficientes | Click en botón antes de que sea clickeable |
Fugas de Recursos | Estado compartido no limpiado | Conexión de base de datos dejada abierta |
Problemas de Concurrencia | Multi-threading, ejecución paralela | Race conditions en ejecuciones paralelas |
Flakes de Infraestructura | Latencia de red, dependencias externas | Timeout de API, caída de servicio 3rd-party |
Código No Determinista | Datos random, timestamps, UUIDs | Test espera valor UUID específico |
Específico de Plataforma | Diferencias de OS, navegador, entorno | Funciona en Chrome, falla en Safari |
Costo de Tests Flaky
Costos directos:
- Tiempo de investigación: 2-8 horas por test flaky
- Retrasos en pipeline CI/CD: Retraso promedio de 15 minutos por re-ejecución
- Confianza falsa: Ignorar fallos reales enmascarados por flakiness
Costos indirectos:
- Frustración del desarrollador y moral disminuida
- Confianza reducida en test suite → saltando tests
- Releases retrasados debido a incertidumbre sobre fallos
Enfoques de Machine Learning para Detección de Tests Flaky
1. Supervised Learning: Modelos de Clasificación
Entrenar un modelo para clasificar tests como “flaky” o “estable” basándose en datos históricos y características de código.
Feature Engineering
Características de historial de ejecución:
execution_features = {
'pass_rate': 0.73, # Pasa 73% del tiempo
'consecutive_failures': 2,
'failure_variability': 0.45, # Alta varianza en resultados
'avg_execution_time': 12.3,
'execution_time_stddev': 4.2, # Alta varianza de tiempo
'last_10_outcomes': [1,1,0,1,1,0,1,1,1,0], # 1=pasa, 0=falla
}
Características basadas en código:
import ast
def extract_code_features(test_code):
tree = ast.parse(test_code)
return {
'uses_sleep': 'time.sleep' in test_code,
'uses_random': 'random' in test_code,
'async_count': test_code.count('async '),
'network_calls': test_code.count('requests.') + test_code.count('httpx.'),
'database_queries': test_code.count('execute('),
'wait_statements': test_code.count('WebDriverWait'),
'thread_usage': test_code.count('Thread('),
'external_deps': test_code.count('mock.') == 0, # Sin mocking
'assertion_count': test_code.count('assert'),
'test_length': len(test_code.split('\n')),
}
Entrenar un Random Forest Classifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
import pandas as pd
# Cargar dataset etiquetado
data = pd.read_csv('test_history.csv')
# Separar características y etiquetas
X = data.drop(['test_id', 'is_flaky'], axis=1)
y = data['is_flaky']
# Dividir datos
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
# Entrenar modelo
model = RandomForestClassifier(
n_estimators=100,
max_depth=10,
random_state=42
)
model.fit(X_train, y_train)
# Evaluar
from sklearn.metrics import classification_report
y_pred = model.predict(X_test)
print(classification_report(y_test, y_pred))
# Importancia de características
feature_importance = pd.DataFrame({
'feature': X.columns,
'importance': model.feature_importances_
}).sort_values('importance', ascending=False)
print("\nPrincipales indicadores de test flaky:")
print(feature_importance.head(10))
2. Unsupervised Learning: Detección de Anomalías
Identificar patrones de comportamiento de test inusuales sin datos etiquetados.
from sklearn.ensemble import IsolationForest
import numpy as np
class FlakyTestAnomalyDetector:
def __init__(self, contamination=0.1):
self.model = IsolationForest(
contamination=contamination,
random_state=42
)
def fit(self, test_execution_history):
features = self._extract_time_series_features(test_execution_history)
self.model.fit(features)
return self
def _extract_time_series_features(self, history):
features = []
for test_id in history['test_id'].unique():
test_data = history[history['test_id'] == test_id]
outcomes = test_data['outcome'].values
times = test_data['execution_time'].values
features.append({
'pass_rate': np.mean(outcomes),
'pass_rate_variance': np.var(outcomes),
'execution_time_mean': np.mean(times),
'execution_time_variance': np.var(times),
})
return pd.DataFrame(features)
def predict_flaky(self, test_execution_history):
features = self._extract_time_series_features(test_execution_history)
predictions = self.model.predict(features)
anomaly_scores = self.model.score_samples(features)
results = pd.DataFrame({
'test_id': test_execution_history['test_id'].unique(),
'anomaly_score': anomaly_scores,
'is_flaky': predictions == -1
})
return results.sort_values('anomaly_score')
3. Análisis de Series Temporales: Reconocimiento de Patrones de Fallo
import tensorflow as tf
from tensorflow import keras
class FlakyTestPredictor:
def __init__(self, sequence_length=20):
self.sequence_length = sequence_length
self.model = self._build_model()
def _build_model(self):
model = keras.Sequential([
keras.layers.LSTM(64, input_shape=(self.sequence_length, 1)),
keras.layers.Dropout(0.2),
keras.layers.Dense(32, activation='relu'),
keras.layers.Dense(1, activation='sigmoid')
])
model.compile(
optimizer='adam',
loss='binary_crossentropy',
metrics=['accuracy']
)
return model
def predict_stability(self, recent_outcomes):
if len(recent_outcomes) < self.sequence_length:
return None
X = np.array(recent_outcomes[-self.sequence_length:])
X = X.reshape(1, self.sequence_length, 1)
prediction = self.model.predict(X, verbose=0)[0][0]
# Alta varianza en predicciones indica flakiness
predictions = []
for _ in range(10):
pred = self.model.predict(X, verbose=0)[0][0]
predictions.append(pred)
prediction_variance = np.var(predictions)
return {
'next_pass_probability': prediction,
'prediction_variance': prediction_variance,
'likely_flaky': prediction_variance > 0.05 or (0.3 < prediction < 0.7)
}
Implementación Práctica
Construyendo un Pipeline de Detección de Tests Flaky
class FlakyTestDetectionPipeline:
def __init__(self):
self.classifier = RandomForestClassifier()
self.anomaly_detector = FlakyTestAnomalyDetector()
def detect_flaky_tests(self, execution_data):
# 1. Análisis estadístico
stats = self._calculate_test_statistics(execution_data)
# 2. Clasificación ML
(como se discute en [AI Test Metrics Analytics: Intelligent Analysis of QA Metrics](/blog/ai-test-metrics)) ml_predictions = self._ml_classification(stats)
# 3. Detección de anomalías
anomalies = self.anomaly_detector.predict_flaky(execution_data)
# Combinar resultados
flaky_tests = self._combine_predictions(stats, ml_predictions, anomalies)
return flaky_tests
def _calculate_test_statistics(self, data):
stats = []
for test_id in data['test_id'].unique():
test_data = data[data['test_id'] == test_id]
outcomes = test_data['outcome'].values
stats.append({
'test_id': test_id,
'pass_rate': np.mean(outcomes),
'total_runs': len(outcomes),
'failures': np.sum(outcomes == 0),
'variance': np.var(outcomes),
})
return pd.DataFrame(stats)
def _combine_predictions(self, stats, ml_preds, anomalies):
results = stats.copy()
# Votación simple: test es flaky si 2+ métodos están de acuerdo
results['statistical_flaky'] = (results['pass_rate'] < 0.95) & (results['pass_rate'] > 0.05)
results['ml_flaky'] = ml_preds if ml_preds is not None else False
results['anomaly_flaky'] = anomalies['is_flaky'].values
results['votes'] = (
results['statistical_flaky'].astype(int) +
results['ml_flaky'].astype(int) +
results['anomaly_flaky'].astype(int)
)
results['is_flaky'] = results['votes'] >= 2
return results[results['is_flaky']].sort_values('pass_rate')
Integración con CI/CD
# .github/workflows/flaky-detection.yml
name: Detección de Tests Flaky
on:
schedule:
- cron: '0 2 * * *' # Diario a las 2 AM
workflow_dispatch:
jobs:
detect-flaky:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Ejecutar tests con detección flaky
run: |
pip install flaky-detector pytest pytest-json-report
pytest --json-report --json-report-file=report.json
- name: Analizar tests flaky
run: |
python scripts/detect_flaky_tests.py --report report.json
- name: Crear issue para tests flaky
if: env.FLAKY_TESTS_FOUND == 'true'
uses: actions/github-script@v6
with:
script: |
const fs = require('fs');
const flaky = JSON.parse(fs.readFileSync('flaky_tests.json'));
let body = '## 🚨 Tests Flaky Detectados\n\n';
flaky.forEach(test => {
body += `### ${test.test_id}\n`;
body += `- Tasa de paso: ${(test.pass_rate * 100).toFixed(1)}%\n`;
body += `- Causa probable: ${test.root_cause}\n\n`;
});
github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: 'Tests Flaky Detectados',
body: body,
labels: ['flaky-test', 'quality']
});
Estrategias para Corregir Tests Flaky
Sugerencias de Corrección Automatizadas
def suggest_fixes(test_code, failure_patterns):
suggestions = []
if 'time.sleep' in test_code:
suggestions.append({
'issue': 'Usa time.sleep (no determinista)',
'fix': 'Reemplazar con WebDriverWait explícito',
'example': 'WebDriverWait(driver, 10).until(EC.element_to_be_clickable((By.ID, "submit")))'
})
if 'click()' in test_code and 'wait' not in test_code.lower():
suggestions.append({
'issue': 'Click sin espera explícita',
'fix': 'Agregar espera antes de hacer click',
'example': 'wait.until(EC.element_to_be_clickable(element)).click()'
})
if 'requests.get' in test_code and 'mock' not in test_code:
suggestions.append({
'issue': 'Dependencia de API externa',
'fix': 'Mockear llamadas de API externas',
'example': '@mock.patch("requests.get")\ndef test_api(mock_get): ...'
})
return suggestions
Midiendo el Éxito
Métricas Clave
Métrica | Antes de ML | Después de ML | Objetivo |
---|---|---|---|
Tiempo de identificación de test flaky | 4-8 horas/test | 5 minutos | < 10 min |
Tasa de falsos positivos en CI | 25% | 8% | < 5% |
Estabilidad del test suite | 75% | 95% | > 98% |
Tiempo de investigación por fallo | 30 min | 10 min | < 15 min |
Tests flaky en producción | 15% | 3% | < 2% |
Cálculo de ROI
Equipo de 10 ingenieros, 5000 tests, 15% tasa flaky
Costo de detección manual:
750 tests flaky × 4 horas investigación = 3,000 horas
3,000 horas × $75/hora = $225,000
Costo de detección ML:
Setup: 80 horas × $75 = $6,000
Mantenimiento: 2 horas/semana × 52 semanas × $75 = $7,800
Total: $13,800
Ahorro: $225,000 - $13,800 = $211,200/año
ROI: 1,530%
Conclusión
Los tests flaky socavan la confianza en la automatización y desperdician recursos significativos de ingeniería. Machine Learning transforma la detección de tests flaky de un proceso reactivo y manual a un sistema proactivo y automatizado que predice flakiness, identifica causas raíz y sugiere correcciones.
Comienza con detección estadística simple, agrega clasificación ML a medida que recopilas datos e integra detección de anomalías para cobertura completa. Rastrea métricas, itera en tus modelos y mejora continuamente la estabilidad de tests.
Recuerda: El objetivo no es solo detectar tests flaky—es prevenirlos. Usa insights de ML para mejorar patrones de diseño de tests, hacer cumplir mejores prácticas y construir una cultura de confiabilidad de tests.
Recursos
- Herramientas: Flaky Test Tracker (Google), DeFlaker, NonDex, FlakeFlagger
- Investigación: “An Empirical Analysis of Flaky Tests” (IEEE), Investigación de Flaky Test de Google
- Datasets: Datasets de test flaky en Zenodo, dataset IDoFT
- Frameworks: pytest-flakefinder, Jest –detectFlakes
Tests estables, despliegues confiados. Deja que ML sea tu guardián de flakiness.