Integrando Continuous Performance Testing en Agile: Estrategias Avanzadas con k6, GitHub Actions y SRE
En mis más de 13 años como Performance Analyst, trabajando con clientes como Bank of America y Toyota, he visto una evolución constante. Sin embargo, el conflicto entre la velocidad de entrega de Agile/DevOps y la necesidad de estabilidad de Performance Engineering sigue siendo uno de los mayores desafíos técnicos.
Antiguamente, teníamos “la ventana de rendimiento” (3 días al mes). Hoy, en empresas HCM o aerolíneas que despliegan varias veces al día, esa ventana no existe. Si tu prueba de carga toma 4 horas en configurarse y ejecutarse, eres el cuello de botella.
Este artículo no es sobre “cómo ejecutar un test”. Es sobre cómo transformar tu disciplina de pruebas en un ciclo de retroalimentación continua. Vamos a desglosar una arquitectura moderna que utiliza k6 para la automatización, GitHub Actions para la orquestación y una mentalidad SRE para el análisis.
El Problema del “Muro de Agua” en Agile
En entornos Agile tradicionales, el testing de rendimiento suele ocurrir en fase UAT o Pre-Prod. Esto crea un “muro de agua” donde los descubrimientos son tardíos y costosos.
Estrategia Clave: Shift-Left de Rendimiento
El Shift-Left no significa correr pruebas de estrés en la máquina de un desarrollador. Significa validar suposiciones de rendimiento temprano:
- Análisis Estático: Revisar código buscando N+1 queries o llamadas síncronas bloqueantes antes de compilar.
- Pruebas de Unidad de Rendimiento: Micro-benchmarks de funciones críticas (ej. cálculo de nómina en una empresa HCM).
- Smoke Tests de Rendimiento en Commit: Validar que el tiempo de respuesta del endpoint crítico no haya degradado más del 5% con el último push.
Selección de Herramientas: ¿Por qué k6 y no JMeter?
Amo JMeter; es el estándar de la industria. Pero para Agile y CI/CD, JMeter tiene desventajas estructurales:
- Curva de aprendizaje: Los desarrolladores en Agile prefieren código sobre GUIs. JMeter (XML) es difícil de mantener en git (resolución de conflictos en XML es pesadilla).
- Consumo de recursos: Simular 10,000 usuarios en modo GUI (incluso en no-GUI) es pesado en comparación con Go o Rust.
k6 (Escrito en Go, scripteable en JS) es la elección moderna para Agile:
- Developer-First: Es JavaScript. Los devs ya lo saben.
- API Friendly: Manejo nativo de JSON, async/await (con limitaciones), y módulos.
- Eficiencia: Un único pod de k6 en Kubernetes puede manejar miles de VUs con un footprint de memoria bajo.
Arquitectura de Solución: Continuous Performance Testing (CPT)
Nuestra arquitectura propuesta:
- Code Commit: Desarrollador sube código a feature branch.
- CI Pipeline (GitHub Actions):
- Ejecuta tests unitarios.
- Trigger: Si pasa, ejecuta Test de Rendimiento Ligero (Smoke) contra entorno de Dev/Staging.
- Gatekeeper:
- Si p(95) < 300ms y Error Rate < 1% -> Pasa.
- Si falla -> Bloquea PR y notifica.
- Monitoring (Datadog/Grafana): Correlaciona las métricas de k6 con APM.
Implementación Técnica: k6 Avanzado
Vamos a escribir un script que no sea un simple “hello world”. Vamos a simular un flujo de negocio complejo: Login -> Búsqueda -> Agregar al Carrito -> Checkout.
1. Estructura del Proyecto
Organiza tus tests modularmente para mantener el código DRY (Don’t Repeat Yourself).
/performance-tests
|-- /lib
| |-- auth.js (Lógica de autenticación)
| |-- navigation.js (Helpers de navegación)
|-- /scenarios
| |-- checkoutFlow.js (El script principal)
|-- k6.config.js (Configuración global)
2. El Script Completo (checkoutFlow.js)
Este script implementa manejo de tokens, pausas realistas y umbrales automáticos.
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate } from 'k6/metrics';
import { SharedArray } from 'k6/data';
import { login } from '../lib/auth.js';
// Configuración de Entornos vía Variables de Entorno
const BASE_URL = __ENV.BASE_URL || 'https://api.miempresa.dev';
// Tasa de error personalizada
const errorRate = new Rate('errors');
// Datos de prueba masivos (Correlation Data)
// Usamos SharedArray para no cargar el archivo en cada VU, ahorrando memoria
const productData = new SharedArray('products', function () {
return JSON.parse(open('../data/products.json')).products;
});
export let options = {
stages: [
{ duration: '2m', target: 100 }, // Ramp up
{ duration: '5m', target: 100 }, // Sustained load
{ duration: '2m', target: 200 }, // Spike
{ duration: '5m', target: 200 }, // Sustained spike
{ duration: '2m', target: 0 }, // Ramp down
],
thresholds: {
http_req_duration: ['p(95)<500', 'p(99)<1000'], // El 95% de reqs debe ser < 500ms
errors: ['rate<0.05'], // Tasa de errores menor al 5%
},
ext: {
loadimpact: {
projectID: 356789, // ID de proyecto en k6 Cloud (opcional)
name: 'Agile Smoke Test',
},
},
};
export function setup() {
// Setup se ejecuta UNA VEZ al inicio por cada instancia de k6
// Útil para obtener tokens de servicio o configurar datos en DB
console.log('Iniciando Setup del Test de Performance...');
// Ejemplo: Llamar a una API de admin para preparar el catálogo
return { authToken: 'service_token_static' }; // Simplificado para ejemplo
}
export default function (data) {
const authHeaders = login('testuser', 'testpass'); // Función helper
// 1. Búsqueda de Producto
let product = productData[Math.floor(Math.random() * productData.length)];
let searchRes = http.get(`${BASE_URL}/products?query=${product.sku}`, {
headers: authHeaders,
tags: { name: 'SearchProduct' },
});
let checkSearch = check(searchRes, {
'Search status is 200': (r) => r.status === 200,
'Search has results': (r) => r.json('results.length') > 0,
});
errorRate.add(!checkSearch);
if (!checkSearch) {
console.error(`Error buscando producto ${product.sku}`);
sleep(1);
return; // Abortar iteración si falla búsqueda
}
sleep(Math.random() * 3 + 2); // Pausa de pensamiento 2-5s
// 2. Agregar al Carrito (POST con Payload Dinámico)
let cartPayload = JSON.stringify({
sku: product.sku,
quantity: 1,
sessionId: `sess-${__VU}-${__ITER}` // Usar ID de VU e Iteración para unicidad
});
let cartRes = http.post(`${BASE_URL}/cart/add`, cartPayload, {
headers: { ...authHeaders, 'Content-Type': 'application/json' },
tags: { name: 'AddToCart' },
});
let checkCart = check(cartRes, {
'Cart status is 201': (r) => r.status === 201,
'Cart response time < 200ms': (r) => r.timings.duration < 200,
});
errorRate.add(!checkCart);
sleep(1);
// 3. Checkout (Transaccional)
let checkoutRes = http.post(`${BASE_URL}/cart/checkout`, '{}', {
headers: authHeaders,
tags: { name: 'Checkout' },
});
check(checkoutRes, {
'Checkout status is 200': (r) => r.status === 200,
});
}
Integración CI/CD: GitHub Actions
Ahora, llevamos esto al pipeline. No queremos ejecutar el test de carga completo (que dura 16 mins en mi configuración) en cada push. Queremos una estrategia Gatekeeper.
Estrategia de Niveles
- Pull Request (PR): Ejecutar carga baja (10 VUs por 1 min). Objetivo: Regresión funcional rápida y chequeo de latencia base.
- Merge to Main: Ejecutar carga completa (Estress Test) en ambiente de Staging.
Configuración de GitHub Actions (.github/workflows/perf-pr.yml)
name: Performance Gatekeeper (PR)
on:
pull_request:
types: [opened, synchronize, reopened]
branches:
- main
- develop
jobs:
k6_performance_test:
name: k6 Load Test (Smoke)
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Install k6
run: |
sudo gpg -k
sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list
sudo apt-get update
sudo apt-get install k6
- name: Run k6 local smoke test
env:
BASE_URL: ${{ secrets.STAGING_URL }} # Usar secrets de GitHub
run: |
k6 run --out json=results.json \
--summary-export=summary.json \
--env BASE_URL=$BASE_URL \
scenarios/checkoutFlow.js
- name: Fail build if thresholds fail
run: |
# k6 retorna código distinto de 0 si falla, pero esto es redundante y explícito
# Aquí podrías parsear summary.json para lógicas complejas
echo "Validating results..."
- name: Publish PR Comment with Results
uses: actions/github-script@v6
if: always()
with:
script: |
const fs = require('fs');
const summary = JSON.parse(fs.readFileSync('summary.json', 'utf8'));
const metrics = summary.metrics;
const p95 = (metrics.http_req_duration.values['p(95)'] / 1000).toFixed(2);
const rps = metrics.http_reqs.rate.toFixed(2);
const failed = metrics.http_req_failed.passes;
const output = `#### ⚡️ K6 Performance Test Results\n\n**Status:** ${failed > 0 ? '❌ FAILED' : '✅ PASSED'}\n\n| Metric | Value |\n| --- | --- |\n| p(95) Latency | ${p95} ms |\n| Requests/sec | ${rps} |\n| Failed Requests | ${failed} |\n\n*Full report attached in artifacts.*`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: output
});
Troubleshooting y Errores Comunes en Agile
1. Ruido en las Métricas (“Flaky Tests”)
Problema: Un PR falla no por tu código, sino porque el entorno de Dev (compartido) está saturado por otro equipo.
Solución: Aislamiento de Entornos. Utiliza Docker Compose o Kubernetes Namespaces para levantar un ambiente efímero para el test. Esto es “Performance Testing como Código”.
# Script de ejemplo para levantar entorno para test
#!/bin/bash
docker-compose -f docker-compose.test.yml up -d
sleep 10 # Esperar salud de la app
BASE_URL=http://localhost:8080 k6 run script.js
docker-compose -f docker-compose.test.yml down
2. Correlación de Datos Dinámicos en APIs
Problema: En Agile las APIs cambian constantemente. Los IDs de sesión o tokens CSRF cambian de estructura.
Solución: No uses expresiones regulares frágiles. Parsea JSON y maneja errores en el script.
// Mal práctica (Regex frágil)
let token = res.html().find("input[name='token']").attr('value');
// Buena práctica (JSON parsing)
try {
let token = res.json('authentication.token');
if (!token) throw new Error('Token not found');
} catch (e) {
console.error('API Contract Broken:', e);
}
Caso de Estudio Real: Aerolínea (Proyecto Confidencial)
Contexto: Una aerolínea cliente (tipo JetBlue) necesitaba renovar su motor de reservas. Los sprints eran de 2 semanas. El equipo de QA funcional usaba Cypress, pero no tenían visibilidad de rendimiento hasta la integración (UAT).
Desafío: En la UAT de la Sprint 12, descubrimos que el servicio de “Búsqueda de Vuelos” tardaba 4 segundos bajo carga. Se debió reescribir el código de caché, retrasando el lanzamiento 3 semanas.
Solución Implementada (My Role as Lead):
- Modularización: Creé una librería de k6 reutilizable que mapeaba los flujos de Cypress a llamadas HTTP de k6.
- Canary Release: Configuramos pruebas de k6 que se ejecutaban automáticamente sobre el nuevo servicio (10% del tráfico) en producción.
- Alertas en Datadog: Si
p(99) > 1sen el canary, alertaba a Slack inmediatamente.
Resultado: En la Sprint 15, un desarrollador cambió un índice en la base de datos. El test de k6 en el Pipeline falló en 3 minutos (antes de mergear). Se evitó una caída potencial en producción. El Lead Time se redujo de 3 semanas a 2 días para arreglos de performance.
Hacia el Future: SRE y Observability
El rendimiento no es solo una métrica de test; es una métrica de negocio (SLA/SLO).
Debemos dejar de medir solo “Throughput” y empezar a medir “Error Budget”.
- SLI: Latencia p(95) del API de pagos.
- SLO: 400ms o menos.
- Error Budget: Si fallamos este SLO, paramos despliegues de nuevas features para corregir estabilidad.
Integrar tus scripts de k6 con Grafana te permite visualizar el impacto de un commit en tu presupuesto de error.
// Ejemplo de integración con Prometheus en k6
import prometheus from 'https://jslib.k6.io/prometheus/0.0.2/index.js';
const registry = new prometheus.Registry();
export default function() {
registry.metrics.get('my_custom_metric').add(1);
}
// Al final del script, exponer para el scraper
export function handleSummary(data) {
return {
'stdout': prometheus.serialize(registry),
};
}
Conclusión
Integrar Performance Testing en Agile no requiere menos disciplina, requiere más automatización. Al adoptar herramientas como k6 que son “developer-first”, al integrarlas en el ciclo de PR con GitHub Actions, y al cambiar el enfoque de “encontrar bugs” a “proteger el presupuesto de error (SRE)”, te conviertes en un facilitador de velocidad, no en un policía de velocidad.
La próxima vez que un PM te pregunte por qué el test tarda 2 días, muéstrale este script de k6 y explícale cómo en 15 minutos pueden validar si el último commit rompió la experiencia del usuario. Ese es el valor real de un Senior Performance Engineer hoy en día.