K6 es una herramienta moderna de load testing amigable para desarrolladores, diseñada para probar el rendimiento y confiabilidad de APIs, microservicios y sitios web. Construido con Go y scriptable en JavaScript, K6 lleva las pruebas de rendimiento al flujo de trabajo DevOps con integración perfecta de CI/CD y capacidades cloud-native.

¿Por Qué K6 para Load Testing Moderno?

K6 aborda las limitaciones de las herramientas tradicionales de pruebas de rendimiento enfocándose en la experiencia del desarrollador y la infraestructura moderna:

  • API JavaScript: Scripting familiar para desarrolladores frontend y backend
  • Diseño CLI-first: Perfecto para automatización y pipelines CI/CD
  • Rendimiento: Uso eficiente de recursos, puede generar alta carga desde una sola máquina
  • Cloud & Local: Ejecutar localmente durante desarrollo, escalar a cloud para pruebas a nivel producción
  • Protocolos modernos: Soporte HTTP/1.1, HTTP/2, WebSockets, gRPC
  • Observabilidad: Métricas en tiempo real, integración Prometheus, dashboards Grafana

K6 vs Herramientas Tradicionales de Load Testing

CaracterísticaK6JMeterGatlingLocust
Lenguaje de ScriptingJavaScriptGUI/XMLScalaPython
Eficiencia de RecursosExcelenteMediaBuenaMedia
Integración CI/CDNativaPluginsBuenaBuena
Curva de AprendizajeBajaMediaAltaBaja
Soporte CloudNativo (K6 Cloud)PluginsGatling EnterpriseNo nativo
Soporte de ProtocolosHTTP/2, WS, gRPCExtensoHTTP/2, WSHTTP
Resultados en Tiempo RealExcelenteLimitadoBuenoBueno

Comenzando con K6

Instalación

# macOS (Homebrew)
brew install k6

# Windows (Chocolatey)
choco install k6

# Linux (Debian/Ubuntu)
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

# Docker
docker pull grafana/k6

Tu Primera Prueba K6

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

export const options = {
  vus: 10, // Virtual users (usuarios virtuales)
  duration: '30s', // Duración de la prueba
};

export default function () {
  const response = http.get('https://test-api.k6.io');

  check(response, {
    'status es 200': (r) => r.status === 200,
    'tiempo de respuesta < 500ms': (r) => r.timings.duration < 500,
  });

  sleep(1);
}

Ejecutar la prueba:

k6 run simple-test.js

Configuración de Pruebas y Perfiles de Carga

Usuarios Virtuales y Duración

export const options = {
  // Configuración simple
  vus: 50,
  duration: '5m',

  // O usar stages para ramping
  stages: [
    { duration: '2m', target: 100 }, // Subir a 100 usuarios
    { duration: '5m', target: 100 }, // Mantener 100 usuarios
    { duration: '2m', target: 0 },   // Bajar a 0 usuarios
  ],
};

Escenarios de Load Testing

export const options = {
  scenarios: {
    // Carga constante
    constant_load: {
      executor: 'constant-vus',
      vus: 50,
      duration: '5m',
    },

    // Ramping VUs
    ramping_load: {
      executor: 'ramping-vus',
      startVUs: 0,
      stages: [
        { duration: '1m', target: 50 },
        { duration: '3m', target: 50 },
        { duration: '1m', target: 0 },
      ],
    },

    // Tasa de solicitudes constante
    constant_rps: {
      executor: 'constant-arrival-rate',
      rate: 1000, // 1000 solicitudes por segundo
      timeUnit: '1s',
      duration: '5m',
      preAllocatedVUs: 50,
      maxVUs: 200,
    },

    // Prueba de spike
    spike_test: {
      executor: 'ramping-arrival-rate',
      startRate: 100,
      timeUnit: '1s',
      preAllocatedVUs: 50,
      maxVUs: 500,
      stages: [
        { duration: '30s', target: 100 },  // Carga normal
        { duration: '1m', target: 1000 },  // Spike
        { duration: '30s', target: 100 },  // Volver a normal
      ],
    },
  },
};

Thresholds para Criterios Pass/Fail

export const options = {
  thresholds: {
    // Errores HTTP deben ser menos del 1%
    http_req_failed: ['rate<0.01'],

    // 95% de solicitudes deben estar bajo 500ms
    http_req_duration: ['p(95)<500'],

    // Percentil 99 debe estar bajo 1s
    'http_req_duration{type:api}': ['p(99)<1000'],

    // Thresholds de métricas personalizadas
    checks: ['rate>0.99'], // 99% de checks deben pasar
  },
};

Pruebas HTTP Avanzadas

Métodos HTTP y Request Bodies

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

export default function () {
  // Solicitud GET
  const getResponse = http.get('https://api.example.com/users');

  // POST con JSON
  const payload = JSON.stringify({
    name: 'John Doe',
    email: 'john@example.com',
  });

  const params = {
    headers: {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer token123',
    },
  };

  const postResponse = http.post(
    'https://api.example.com/users',
    payload,
    params
  );

  check(postResponse, {
    'creado exitosamente': (r) => r.status === 201,
    'tiene user id': (r) => r.json('id') !== undefined,
  });

  // Solicitud PUT
  const updatePayload = JSON.stringify({ name: 'Jane Doe' });
  http.put(`https://api.example.com/users/${postResponse.json('id')}`, updatePayload, params);

  // Solicitud DELETE
  http.del(`https://api.example.com/users/${postResponse.json('id')}`, null, params);
}

Batch Requests

import http from 'k6/http';

export default function () {
  // Ejecutar solicitudes en paralelo
  const responses = http.batch([
    ['GET', 'https://api.example.com/users'],
    ['GET', 'https://api.example.com/products'],
    ['GET', 'https://api.example.com/orders'],
  ]);

  // Verificar todas las respuestas
  responses.forEach((response, index) => {
    check(response, {
      [`solicitud ${index} status es 200`]: (r) => r.status === 200,
    });
  });
}

Autenticación y Gestión de Sesiones

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

export function setup() {
  // Login una vez por ejecución de prueba
  const loginResponse = http.post('https://api.example.com/auth/login', {
    username: 'testuser',
    password: 'testpass',
  });

  return { token: loginResponse.json('token') };
}

export default function (data) {
  // Usar token desde setup
  const params = {
    headers: {
      'Authorization': `Bearer ${data.token}`,
    },
  };

  const response = http.get('https://api.example.com/protected', params);

  check(response, {
    'autenticado': (r) => r.status === 200,
  });
}

Parametrización de Datos

Usando Datos CSV

import { SharedArray } from 'k6/data';
import papaparse from 'https://jslib.k6.io/papaparse/5.1.1/index.js';

const csvData = new SharedArray('users', function () {
  return papaparse.parse(open('./users.csv'), { header: true }).data;
});

export default function () {
  // Obtener usuario aleatorio
  const user = csvData[Math.floor(Math.random() * csvData.length)];

  const response = http.post('https://api.example.com/login', {
    username: user.username,
    password: user.password,
  });

  check(response, {
    'login exitoso': (r) => r.status === 200,
  });
}

Usando Datos JSON

import { SharedArray } from 'k6/data';

const testData = new SharedArray('products', function () {
  return JSON.parse(open('./products.json'));
});

export default function () {
  const product = testData[__VU % testData.length]; // Selección round-robin

  http.post('https://api.example.com/cart/add', JSON.stringify({
    productId: product.id,
    quantity: 1,
  }), {
    headers: { 'Content-Type': 'application/json' },
  });
}

Métricas Personalizadas y Tags

Métricas Personalizadas

import http from 'k6/http';
import { Trend, Counter, Rate, Gauge } from 'k6/metrics';

const myTrend = new Trend('waiting_time');
const myCounter = new Counter('my_counter');
const myRate = new Rate('my_rate');
const myGauge = new Gauge('my_gauge');

export default function () {
  const response = http.get('https://api.example.com');

  // Agregar métricas personalizadas
  myTrend.add(response.timings.waiting);
  myCounter.add(1);
  myRate.add(response.status === 200);
  myGauge.add(response.timings.duration);
}

Tags para Organizar Métricas

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

export default function () {
  // Etiquetar solicitudes individuales
  const response = http.get('https://api.example.com/users', {
    tags: {
      type: 'api',
      endpoint: 'users',
    },
  });

  check(response, {
    'status 200': (r) => r.status === 200,
  }, { type: 'api' }); // Etiquetar checks
}

export const options = {
  thresholds: {
    // Threshold para tags específicos
    'http_req_duration{type:api}': ['p(95)<500'],
    'http_req_duration{endpoint:users}': ['p(99)<1000'],
  },
};

Escenarios de Prueba del Mundo Real

Flujo de Usuario E-commerce

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

export const options = {
  stages: [
    { duration: '2m', target: 50 },
    { duration: '5m', target: 50 },
    { duration: '2m', target: 0 },
  ],
  thresholds: {
    http_req_duration: ['p(95)<500'],
    'http_req_duration{page:home}': ['p(95)<300'],
    'http_req_duration{page:product}': ['p(95)<400'],
  },
};

export default function () {
  let response;

  group('Página de Inicio', function () {
    response = http.get('https://ecommerce.example.com', {
      tags: { page: 'home' },
    });
    check(response, { 'homepage cargada': (r) => r.status === 200 });
    sleep(1);
  });

  group('Buscar Productos', function () {
    response = http.get('https://ecommerce.example.com/search?q=laptop', {
      tags: { page: 'search' },
    });
    check(response, { 'resultados de búsqueda': (r) => r.status === 200 });
    sleep(2);
  });

  group('Ver Producto', function () {
    response = http.get('https://ecommerce.example.com/products/123', {
      tags: { page: 'product' },
    });
    check(response, { 'detalles del producto': (r) => r.status === 200 });
    sleep(3);
  });

  group('Agregar al Carrito', function () {
    response = http.post(
      'https://ecommerce.example.com/cart',
      JSON.stringify({ productId: 123, quantity: 1 }),
      {
        headers: { 'Content-Type': 'application/json' },
        tags: { page: 'cart' },
      }
    );
    check(response, { 'agregado al carrito': (r) => r.status === 200 });
    sleep(1);
  });

  group('Checkout', function () {
    response = http.post(
      'https://ecommerce.example.com/checkout',
      JSON.stringify({
        payment: 'card',
        address: '123 Main St',
      }),
      {
        headers: { 'Content-Type': 'application/json' },
        tags: { page: 'checkout' },
      }
    );
    check(response, { 'checkout exitoso': (r) => r.status === 200 });
  });
}

Prueba de Carga API con Manejo de Errores

import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate } from 'k6/metrics';

const errorRate = new Rate('errors');

export const options = {
  scenarios: {
    stress_test: {
      executor: 'ramping-arrival-rate',
      startRate: 10,
      timeUnit: '1s',
      preAllocatedVUs: 50,
      maxVUs: 500,
      stages: [
        { duration: '1m', target: 10 },
        { duration: '2m', target: 50 },
        { duration: '2m', target: 100 },
        { duration: '1m', target: 10 },
      ],
    },
  },
  thresholds: {
    errors: ['rate<0.1'], // Tasa de error debe ser menor al 10%
    http_req_duration: ['p(95)<2000', 'p(99)<5000'],
  },
};

export default function () {
  const response = http.get('https://api.example.com/data');

  const success = check(response, {
    'status es 200': (r) => r.status === 200,
    'tiempo de respuesta OK': (r) => r.timings.duration < 1000,
    'sin errores en body': (r) => !r.body.includes('error'),
  });

  errorRate.add(!success);

  if (response.status !== 200) {
    console.error(`Solicitud falló: ${response.status} ${response.body}`);
  }

  sleep(1);
}

Integración CI/CD

GitHub Actions

name: Load Test

on:
  push:
    branches: [main]
  schedule:
    - cron: '0 2 * * *' # Diariamente a las 2 AM

jobs:
  k6_load_test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Run K6 Load Test
        uses: grafana/k6-action@v0.3.1
        with:
          filename: tests/load-test.js
          cloud: true
          token: ${{ secrets.K6_CLOUD_TOKEN }}

      - name: Upload Results
        uses: actions/upload-artifact@v3
        if: always()
        with:
          name: k6-results
          path: results/

Jenkins Pipeline

pipeline {
    agent any

    stages {
        stage('Load Test') {
            steps {
                sh '''
                    k6 run --out json=results.json \
                           --out influxdb=http://influxdb:8086/k6 \
                           tests/load-test.js
                '''
            }
        }

        stage('Check Thresholds') {
            steps {
                script {
                    def exitCode = sh(
                        script: 'k6 run --quiet tests/load-test.js',
                        returnStatus: true
                    )
                    if (exitCode != 0) {
                        error("Los thresholds de load test fallaron")
                    }
                }
            }
        }
    }

    post {
        always {
            archiveArtifacts artifacts: 'results.json', fingerprint: true
        }
    }
}

GitLab CI

k6-test:
  stage: test
  image: grafana/k6:latest
  script:
    - k6 run --summary-export=summary.json tests/load-test.js
  artifacts:
    when: always
    paths:
      - summary.json
    reports:
      junit: summary.json

Observabilidad y Análisis de Resultados

Opciones de Output

# Salida JSON
k6 run --out json=results.json script.js

# Salida CSV
k6 run --out csv=results.csv script.js

# InfluxDB + Grafana
k6 run --out influxdb=http://localhost:8086/k6 script.js

# Prometheus Remote Write
k6 run --out experimental-prometheus-rw script.js

# K6 Cloud
k6 run --out cloud script.js

# Múltiples salidas
k6 run --out json=results.json --out influxdb=http://localhost:8086/k6 script.js

Lectura de Resultados

# Estadísticas resumen
k6 run script.js

# Salida detallada
k6 run --verbose script.js

# Modo silencioso (solo errores)
k6 run --quiet script.js

# Guardar resumen a archivo
k6 run --summary-export=summary.json script.js

Resumen Personalizado

import { htmlReport } from "https://raw.githubusercontent.com/benc-uk/k6-reporter/main/dist/bundle.js";
import { textSummary } from "https://jslib.k6.io/k6-summary/0.0.1/index.js";

export function handleSummary(data) {
  return {
    "summary.html": htmlReport(data),
    "summary.json": JSON.stringify(data),
    stdout: textSummary(data, { indent: " ", enableColors: true }),
  };
}

Mejores Prácticas

Eficiencia de Recursos

// ❌ Mal: Crea nuevo objeto cada iteración
export default function () {
  const headers = { 'Content-Type': 'application/json' };
  http.get('https://api.example.com', { headers });
}

// ✓ Bien: Reutilizar objetos
const params = {
  headers: { 'Content-Type': 'application/json' },
};

export default function () {
  http.get('https://api.example.com', params);
}

Think Time y Pacing

import { sleep } from 'k6';

export default function () {
  // Simular tiempo de pensamiento del usuario
  http.get('https://api.example.com/page1');
  sleep(Math.random() * 3 + 2); // 2-5 segundos

  http.get('https://api.example.com/page2');
  sleep(Math.random() * 2 + 1); // 1-3 segundos
}

Scripts de Prueba Modulares

// modules/auth.js
export function login(username, password) {
  const response = http.post('https://api.example.com/login', {
    username,
    password,
  });
  return response.json('token');
}

// modules/users.js
export function getUser(token, userId) {
  return http.get(`https://api.example.com/users/${userId}`, {
    headers: { Authorization: `Bearer ${token}` },
  });
}

// main-test.js
import { login } from './modules/auth.js';
import { getUser } from './modules/users.js';

export default function () {
  const token = login('user', 'pass');
  getUser(token, 123);
}

Conclusión

K6 representa un cambio de paradigma en pruebas de rendimiento, trayendo herramientas amigables para desarrolladores y prácticas modernas al load testing. Su API JavaScript, uso eficiente de recursos e integración perfecta con pipelines CI/CD lo hacen ideal para equipos DevOps que practican pruebas continuas.

Ventajas clave:

  • API JavaScript amigable para desarrolladores
  • Uso eficiente de recursos para generación de alta carga
  • Integración nativa CI/CD
  • Soporte de protocolos modernos (HTTP/2, WebSockets, gRPC)
  • Modos de ejecución flexibles (local, cloud, híbrido)
  • Rico ecosistema y opciones de observabilidad

Ya sea que estés probando microservicios, APIs o aplicaciones web completas, K6 proporciona las herramientas y flexibilidad para asegurar que tus sistemas funcionen confiablemente bajo carga. Al integrar pruebas de rendimiento temprano en el ciclo de desarrollo, los equipos pueden detectar problemas antes de que lleguen a producción y mantener aplicaciones de alta calidad y rendimiento.