Una consulta de base de datos que corre en 30ms con 100 filas puede hacer timeout con un millón, y los tests funcionales nunca lo van a detectar porque ejecutan un escenario a la vez. El testing de performance simula el tráfico concurrente que revela estos fallos: load testing para tráfico normal, stress testing para encontrar el punto de quiebre, spike testing para picos súbitos, y soak testing para exponer fugas de memoria que solo aparecen después de horas. Esta guía cubre k6, la herramienta basada en JavaScript que ejecuta load tests desde la CLI, termina con código no cero cuando los umbrales no se cumplen, y se integra directamente en GitHub Actions.

Por qué importa el testing de performance

Un formulario de login que tarda 200ms está bien. Uno que tarda 5 segundos pierde usuarios. Uno que tarda 30 segundos bajo carga tira el servidor.

Los bugs de performance suelen ser los más difíciles de corregir: requieren cambios arquitecturales, no simples correcciones de código. Encontrarlos temprano importa.

Fallos de performance comunes

  • Consultas de base de datos que funcionan bien con 100 filas, hacen timeout con un millón
  • Endpoints de API que manejan 10 usuarios concurrentes, fallan con 100
  • Fugas de memoria que se acumulan durante horas de uso
  • Integraciones de terceros que se convierten en cuellos de botella bajo carga

Tipos de tests de performance

Load testing

Simula la carga esperada en producción para verificar que el sistema rinde dentro de límites aceptables.

Pregunta: ¿El sistema maneja el tráfico normal? Ejemplo: La app tiene 500 usuarios concurrentes durante el horario laboral. Ejecuta 500 usuarios virtuales durante 30 minutos y mide los tiempos de respuesta.

Resultados aceptables

Tiempo de respuesta en el percentil 95 menor a 1 segundo, tasa de errores menor al 1% y sin fugas de memoria.

Stress testing

Llevar el sistema más allá de sus límites para encontrar el punto de quiebre.

Pregunta: ¿Cómo se comporta el sistema cuando está sobrecargado? ¿Falla de forma controlada? Ejemplo: Empezar con 100 usuarios, aumentar en 100 cada minuto hasta que el sistema rompa. Observar cuándo empiezan los errores, cómo falla el sistema y si se recupera.

Spike testing

Aumento masivo y súbito de carga, luego regreso a la normalidad.

Pregunta: ¿El sistema puede manejar picos repentinos de tráfico? Ejemplo: La carga normal es de 100 usuarios. Saltar de repente a 1.000 durante 2 minutos, luego volver a 100. Contexto relevante: Menciones en redes sociales, noticias, ventas flash.

Soak / endurance testing

Carga sostenida durante un período extendido.

Pregunta: ¿Hay fugas de memoria o degradación del rendimiento a lo largo del tiempo? Ejemplo: 200 usuarios concurrentes durante 8 horas. Monitorear el uso de memoria y la tendencia de los tiempos de respuesta.

k6: la herramienta moderna de load testing

k6 es la herramienta de load testing más amigable para QA: basada en JavaScript, corre desde la CLI, se integra con CI.

Instalación

# macOS
brew install k6

# Windows (winget)
winget install k6

# Docker
docker pull grafana/k6

Tu primer load test

// load-test.js
import http from 'k6/http';
import { check, sleep } from 'k6';

// Configuración del test
export const options = {
  vus: 50,           // Usuarios virtuales
  duration: '30s',  // Ejecutar durante 30 segundos
};

export default function() {
  // Hacer un request
  const response = http.get('https://lab.becomeqa.com/api/products');
  
  // Aserciones
  check(response, {
    'status es 200': (r) => r.status === 200,
    'tiempo de respuesta < 500ms': (r) => r.timings.duration < 500,
    'tiene productos': (r) => JSON.parse(r.body).length > 0,
  });
  
  // Esperar entre iteraciones (simula comportamiento real del usuario)
  sleep(1);
}

Ejecutar:

k6 run load-test.js

Salida:

✓ status es 200               100.00% ✓ 1500  ✗ 0
✓ tiempo de respuesta < 500ms  95.33%  ✓ 1430  ✗ 70
✓ tiene productos             100.00% ✓ 1500  ✗ 0

http_req_duration..............: avg=185ms min=45ms  med=160ms  max=1.2s    p(90)=350ms p(95)=480ms
http_reqs......................: 1500   49.91/s

Escenarios de ramp-up

Más realista que llegar a la carga total de inmediato:

export const options = {
  stages: [
    { duration: '2m', target: 100 },  // Subir a 100 usuarios en 2 minutos
    { duration: '5m', target: 100 },  // Mantener 100 usuarios por 5 minutos
    { duration: '2m', target: 200 },  // Subir a 200
    { duration: '5m', target: 200 },  // Mantener 200 usuarios
    { duration: '2m', target: 0 },    // Bajar gradualmente
  ],
};

Testear un endpoint de API con autenticación

import http from 'k6/http';
import { check, group } from 'k6';

export const options = {
  stages: [
    { duration: '1m', target: 50 },
    { duration: '3m', target: 50 },
    { duration: '1m', target: 0 },
  ],
  thresholds: {
    'http_req_duration': ['p(95)<500'],  // 95% de requests bajo 500ms
    'http_req_failed': ['rate<0.01'],     // Menos del 1% de errores
  },
};

export function setup() {
  // Login una vez, devolver token para todos los usuarios virtuales
  const res = http.post('https://api.miapp.com/auth/login', JSON.stringify({
    email: 'load-test@miapp.com',
    password: 'PassLoadTest1',
  }), { headers: { 'Content-Type': 'application/json' } });
  
  return { token: JSON.parse(res.body).token };
}

export default function(data) {
  const headers = {
    'Authorization': `Bearer ${data.token}`,
    'Content-Type': 'application/json',
  };
  
  group('Listar productos', () => {
    const res = http.get('https://api.miapp.com/products', { headers });
    check(res, { 'productos cargados': (r) => r.status === 200 });
  });
  
  group('Detalle de producto', () => {
    const res = http.get('https://api.miapp.com/products/1', { headers });
    check(res, { 'detalle cargado': (r) => r.status === 200 });
  });
  
  sleep(Math.random() * 3 + 1);  // Tiempo de espera aleatorio: 1 a 4 segundos
}

Umbrales de performance

Definir criterios de aprobación/rechazo:

export const options = {
  thresholds: {
    // 95% de requests deben completarse en menos de 500ms
    'http_req_duration': ['p(95)<500'],
    
    // Umbral específico por endpoint
    'http_req_duration{name:products}': ['p(95)<300'],
    
    // Menos del 0.1% de errores permitidos
    'http_req_failed': ['rate<0.001'],
    
    // Umbral de métrica personalizada
    'checkout_duration': ['p(90)<2000'],
  },
};

Si no se cumplen los umbrales, k6 termina con código no cero y el CI falla automáticamente.

Qué medir

Percentiles de tiempo de respuesta

  • p50 (mediana): el 50% de los requests son más rápidos que esto
  • p90: el 90% de los requests son más rápidos que esto
  • p95: el 95% de los requests son más rápidos que esto
  • p99: la cola lenta, lo que experimenta el peor 1%
Tasa de errores: porcentaje de requests que fallan (5xx, timeouts) Throughput: requests por segundo que maneja el sistema Usuarios concurrentes: cuántos usuarios están activos simultáneamente Utilización de recursos: CPU, memoria, conexiones a la base de datos (medido del lado del servidor)

Integración con CI

# GitHub Actions
performance-test:
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4
    
    - name: Instalar k6
      run: |
        curl https://github.com/grafana/k6/releases/download/v0.50.0/k6-v0.50.0-linux-amd64.tar.gz -L | tar xvz
        sudo mv k6-v0.50.0-linux-amd64/k6 /usr/local/bin
    
    - name: Ejecutar load test
      run: k6 run load-tests/api-load-test.js
      env:
        BASE_URL: ${{ secrets.STAGING_URL }}
    
    - name: Subir resultados
      uses: actions/upload-artifact@v4
      if: always()
      with:
        name: k6-results
        path: k6-results.json

Problemas de performance comunes que buscar

Consultas lentas a la base de datos

Monitorear el tiempo de ejecución de queries bajo carga, consultas N+1 (1 query por fila en lugar de 1 en total) e índices faltantes.

Fugas de memoria

El uso de memoria crece con el tiempo sin disminuir. Se detectan en los soak tests.

Agotamiento del connection pool

Las conexiones a la base de datos se agotan con alta concurrencia, produciendo el error "too many connections".

Cuellos de botella en servicios de terceros

Una API externa que se vuelve lenta bajo carga o una pasarela de pago con rate limits.

Caché ausente

Los mismos datos se obtienen de la base de datos repetidamente y deberían estar en caché.

Resumen

| Tipo de test | Propósito | Duración |

|-------------|-----------|---------|

| Load test | Verificar performance con carga normal | 30min-2h |

| Stress test | Encontrar el punto de quiebre | Hasta el fallo |

| Spike test | Manejar picos repentinos de tráfico | Picos cortos |

| Soak test | Detectar fugas de memoria | Horas |

Conceptos básicos de k6

  • vus: usuarios virtuales
  • duration: cuánto tiempo ejecutar
  • stages: patrones de ramp-up
  • thresholds: criterios de aprobación/rechazo
  • check(): aserciones por request
→ See also: Pruebas de Rendimiento con k6: La Primera Prueba de Carga del Ingeniero QA | La Pirámide de Tests Explicada para Ingenieros QA | API Testing 101: Todo lo que Todo QA Engineer Necesita Saber en 2026