El Problema de Explosión de Suites de Tests
Las aplicaciones modernas tienen miles de tests automatizados. Ejecutar todos los tests en cada commit es lento (horas), costoso y retrasa el feedback. Ejecutar muy pocos tests arriesga perder bugs que llegan a producción.
La selección predictiva de tests usa ML (como se discute en AI-powered Test Generation: The Future Is Already Here) para elegir inteligentemente qué tests ejecutar basándose en cambios de código, fallos históricos y análisis de riesgo—reduciendo tiempo de ejecución en 60-90% mientras mantiene calidad.
Cómo Funciona la Selección Predictiva
1. Mapeo Código-Test
class MapeadorCodigoTest:
def __init__(self):
self.mapa_codigo_test = defaultdict(set)
self.cobertura_test = {}
def analizar_cobertura(self, datos_ejecucion_test):
"""Construir mapeo desde datos de cobertura"""
for nombre_test, datos_cobertura in datos_ejecucion_test.items():
archivos_cubiertos = datos_cobertura['archivos']
for ruta_archivo in archivos_cubiertos:
self.mapa_codigo_test[ruta_archivo].add(nombre_test)
def obtener_tests_afectados(self, archivos_cambiados):
"""Obtener tests afectados por cambios de código"""
afectados = set()
for ruta_archivo in archivos_cambiados:
afectados.update(self.mapa_codigo_test.get(ruta_archivo, set()))
return list(afectados)
2. Modelo de Predicción de Fallos
from sklearn.ensemble import RandomForestClassifier
class PredictorFallosTest:
def __init__(self):
self.modelo = RandomForestClassifier(n_estimators=100)
def extraer_caracteristicas(self, commit, test):
"""Extraer características para predicción"""
return {
'archivos_cambiados': len(commit['archivos']),
'lineas_agregadas': commit['adiciones'],
'lineas_eliminadas': commit['eliminaciones'],
'tiempo_ejecucion_test_ms': test['duracion_promedio'],
'puntaje_inestabilidad_test': test['inestabilidad'],
'tasa_fallo_30d': test['fallos_ultimos_30_dias'] / test['ejecuciones_ultimos_30_dias']
}
def predecir_probabilidad_fallo(self, commit, test):
"""Predecir probabilidad de que test falle"""
caracteristicas = self.extraer_caracteristicas(commit, test)
vector_caracteristicas = [list(caracteristicas.values())]
probabilidad = self.modelo.predict_proba(vector_caracteristicas)[0][1]
return {
'test': test['nombre'],
'probabilidad_fallo': probabilidad
}
3. Priorización de Tests
class PriorizadorTests:
def calcular_valor_test(self, test, commit):
"""Calcular puntuación de valor para test"""
prob_fallo = self.predictor.predecir_probabilidad_fallo(commit, test)['probabilidad_fallo']
cobertura_codigo = test['cobertura_lineas'] / lineas_totales
historial_deteccion_bugs = test['bugs_atrapados_ultimo_año']
costo_ejecucion = test['duracion_promedio_ms'] / 1000
# Valor = (Riesgo Fallo × Cobertura × Historial Bugs) / Costo
puntuacion_valor = (prob_fallo * cobertura_codigo * historial_deteccion_bugs) / max(costo_ejecucion, 1)
return puntuacion_valor
def priorizar(self, commit, presupuesto_tiempo_segundos):
"""Seleccionar tests para maximizar valor dentro de presupuesto de tiempo"""
todos_tests = self.mapeador.obtener_catalogo_tests()
# Calcular valor para cada test
puntuaciones_tests = [
{
'test': test,
'valor': self.calcular_valor_test(test, commit),
'duracion': test['duracion_promedio_ms'] / 1000
}
for test in todos_tests
]
# Ordenar por valor (descendente)
puntuaciones_tests.sort(key=lambda x: x['valor'], reverse=True)
# Selección greedy dentro de presupuesto
tests_seleccionados = []
tiempo_total = 0
for item in puntuaciones_tests:
if tiempo_total + item['duracion'] <= presupuesto_tiempo_segundos:
tests_seleccionados.append(item['test'])
tiempo_total += item['duracion']
return {
'tests_seleccionados': tests_seleccionados,
'duracion_estimada': tiempo_total,
'cobertura': len(tests_seleccionados) / len(todos_tests)
}
Integración CI/CD
Ejemplo GitHub Actions
name: Selección Inteligente de Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Analizar Cambios de Código
id: changes
run: |
CHANGED_FILES=$(git diff --name-only ${{ github.event.before }} ${{ github.sha }})
echo "files=$CHANGED_FILES" >> $GITHUB_OUTPUT
- name: Predecir Selección de Tests
id: selection
run: |
python predecir_tests.py \
--archivos-cambiados "$CHANGED_FILES" \
--presupuesto-tiempo 600
- name: Ejecutar Tests Seleccionados
run: |
pytest $(cat tests_seleccionados.json | jq -r '.tests[]')
Análisis de Impacto de Tests
class AnalizadorImpactoTests:
def __init__(self):
self.grafo_impacto = nx.DiGraph()
def construir_grafo_impacto(self, base_codigo):
"""Construir grafo de dependencias"""
for archivo in base_codigo.archivos:
self.grafo_impacto.add_node(archivo.ruta, tipo='codigo')
for test in base_codigo.tests:
self.grafo_impacto.add_node(test.nombre, tipo='test')
for archivo_cubierto in test.cobertura:
self.grafo_impacto.add_edge(archivo_cubierto, test.nombre)
def obtener_tests_impactados(self, archivos_cambiados):
"""Encontrar todos los tests impactados transitivamente"""
impactados = set()
for archivo_cambiado in archivos_cambiados:
alcanzables = nx.descendants(self.grafo_impacto, archivo_cambiado)
for nodo in alcanzables:
if self.grafo_impacto.nodes[nodo]['tipo'] == 'test':
impactados.add(nodo)
return list(impactados)
Métricas y Monitoreo
class MetricasSeleccion:
def registrar_seleccion(self, commit, seleccionados, omitidos, resultados):
"""Registrar efectividad de selección"""
fallos_seleccionados = [t for t in seleccionados if resultados[t] == 'fallido']
fallos_omitidos = [t for t in omitidos if resultados[t] == 'fallido']
self.metricas.append({
'commit': commit,
'tests_seleccionados': len(seleccionados),
'tests_omitidos': len(omitidos),
'tiempo_ahorrado_porcentaje': len(omitidos) / (len(seleccionados) + len(omitidos)),
'fallos_capturados': len(fallos_seleccionados),
'fallos_perdidos': len(fallos_omitidos),
'precision': len(fallos_seleccionados) / len(seleccionados) if seleccionados else 0,
'recall': len(fallos_seleccionados) / (len(fallos_seleccionados) + len(fallos_omitidos)) if (fallos_seleccionados or fallos_omitidos) else 1.0
})
Mejores Prácticas
Práctica | Descripción |
---|---|
Empezar Conservador | Comenzar con recall alto (95%+) |
Monitorear Fallos Perdidos | Rastrear falsos negativos |
Reentrenar Regularmente | Actualizar modelo semanalmente |
Siempre Ejecutar Tests Críticos | Seguridad, smoke tests siempre |
Ciclo de Feedback | Registrar resultados para mejorar |
Despliegue Gradual | Validar en subconjunto primero |
Conclusión
La selección predictiva de tests transforma CI/CD de “ejecutar todo y esperar” a bucles de feedback inteligentes y rápidos. Al combinar análisis de código, predicción ML (como se discute en AI Test Metrics Analytics: Intelligent Analysis of QA Metrics) y priorización basada en riesgo, los equipos reducen tiempo de ejecución de tests en 60-90% mientras capturan 95%+ de fallos.