Módulo 8: Proyecto — Prueba de carga de la API de Reservo

8. Proyecto: la prueba de carga completa de Reservo

Descripción

Este es el entregable. En las siete lecciones anteriores construiste cada pieza; aquí las juntas todas en una prueba de carga completa de la API de Reservo y la entregas. Cuatro piezas que encajan: el script k6 completo (contenido) —el escenario cotizar→reservar con check(), sleep() con jitter, stages smoke→load→stress y thresholds ligados al SLO—, la API canónica como blanco, la corrida ejecutable en Python con sus métricas y su gate verde y rojo (con exit codes reales y el results.json exportado), y el load.yml de CI (contenido). Cierras con una rúbrica de lo que hace buena a una prueba de carga. Y como es la última lección, cierras también la guía: el arco de los ocho módulos y a dónde seguir.

Conexión con el módulo: esta lección es la síntesis. No añade nada nuevo; ensambla las cuatro piezas de las lecciones 2 a 7 en un artefacto entregable y lo juzga contra una rúbrica. Es la sinfonía completa de la que hablamos en la lección 1: cada sección tocando a la vez, en orden, formando una sola pieza. Al terminar tendrás —y sabrás explicar— una prueba de carga de principio a fin, y habrás cerrado el recorrido de la guía.

Lo que vas a entregar

Un proyecto con cuatro archivos que forman la prueba de carga completa:

reservo-load-test/
├── reservo_server.py       # la API canónica de Reservo (el blanco)
├── quote_book_test.js      # el script de k6 (CONTENIDO: k6 no está instalado)
├── loadtest.py             # la corrida ejecutable: escenario + stages + gate + export
└── .github/workflows/
    └── load.yml            # el CI (CONTENIDO): el threshold como gate del deploy

Los recorremos en orden, y al final corremos la prueba en sus dos caras (verde y roja) y la juzgamos con la rúbrica.

Pieza 1 — El script de k6 completo (contenido)

Este es el entregable de k6: el escenario, el perfil y los thresholds de las lecciones 2, 3 y 4, en un solo archivo. Contenido rotulado, fiel a la documentación de k6, no ejecutado aquí (k6 no está instalado):

// quote_book_test.js — la prueba de carga completa de Reservo (CAPSTONE).
// CONTENIDO (no ejecutado aquí): k6 no está instalado.
// Se correría con:  k6 run quote_book_test.js --out json=results.json
// Referencia: grafana.com/docs/k6
import http from 'k6/http';
import { check, sleep } from 'k6';

const BASE_URL = __ENV.BASE_URL || 'http://127.0.0.1:8000';

// Datos parametrizados (M6): salas, tarifas (centavos/hora) y planes.
const ROOMS = ['Focus', 'Studio', 'Boardroom'];
const RATES = { Focus: 2500, Studio: 4000, Boardroom: 8000 };
const TIERS = ['basic', 'pro'];
function pick(a) { return a[Math.floor(Math.random() * a.length)]; }
function expectedPrice(room, tier, hours) {
  let total = RATES[room] * hours;
  if (tier === 'pro') total = Math.floor((total * 80) / 100); // descuento pro entero
  return total;
}

export const options = {
  // PERFIL (M4): smoke -> load -> stress -> ramp-down.
  stages: [
    { duration: '30s', target: 5 },   // smoke
    { duration: '1m',  target: 5 },
    { duration: '1m',  target: 20 },  // load
    { duration: '3m',  target: 20 },
    { duration: '1m',  target: 80 },  // stress
    { duration: '3m',  target: 80 },
    { duration: '1m',  target: 0 },   // ramp-down
  ],
  // THRESHOLDS ligados al SLO (M5). Un threshold roto -> k6 sale con 99 -> gate.
  thresholds: {
    http_req_duration: ['p(95)<200'],  // latencia
    http_req_failed: ['rate<0.01'],    // disponibilidad
    checks: ['rate>0.99'],             // corrección
  },
};

// ESCENARIO (M2/M6): cotizar -> reservar, con checks y think time con jitter.
export default function () {
  const room = pick(ROOMS);
  const tier = pick(TIERS);
  const hours = Math.floor(Math.random() * 8) + 1;
  const want = expectedPrice(room, tier, hours);
  const params = { headers: { 'Content-Type': 'application/json' } };

  const quote = http.post(`${BASE_URL}/quote`, JSON.stringify({ room, tier, hours }), params);
  check(quote, {
    'quote status is 200': (r) => r.status === 200,
    'quote price is correct': (r) => r.json('price_cents') === want,
  });

  const book = http.post(`${BASE_URL}/book`, JSON.stringify({ room, tier, hours }), params);
  check(book, {
    'book status is 200': (r) => r.status === 200,
    'book is confirmed': (r) => r.json('confirmed') === true,
    'book has booking_id': (r) => typeof r.json('booking_id') === 'string',
  });

  sleep(0.5 + Math.random() * 0.5); // think time con jitter
}

Todo el capstone en un archivo: el options con stages (M4) y thresholds (M5), y la función default con el escenario cotizar→reservar, sus cinco check() de corrección (M2, M6) y el sleep() con jitter (M6). Un equipo con k6 instalado lo correría con k6 run quote_book_test.js --out json=results.json.

Pieza 2 — La API canónica (el blanco)

El blanco es la API de Reservo del módulo 1, reusada sin cambios, más el endpoint /quote_cpu que reusamos de M5 para poder ver el gate rojo. Ya la conoces: GET /rooms, POST /quote{price_cents}, POST /book{booking_id, confirmed}, con los números-ancla 7500 y 6000, dinero en centavos enteros, puerto 0. No la repetimos aquí (está en el módulo 1); solo recordamos que es lo que las otras piezas martillan.

Pieza 3 — La corrida ejecutable con su gate (verde y rojo)

Y esta es la pieza que sí se ejecuta: loadtest.py, el generador que corre el escenario cotizar→reservar por etapas contra la API, mide p50/p95/p99, RPS y tasa de error, evalúa los thresholds sobre la corrida entera, exporta las métricas a JSON y sale con exit code. Es el espejo ejecutable del script k6. Lo corremos en sus dos caras.

El gate verde (build sano, /quote).

Qué esperar — con el endpoint rápido, hasta el pico da un p95 de decenas de milisegundos; el agregado queda muy bajo el SLO; los tres thresholds pasan; exit 0. Salida real:

$ python3.14 loadtest.py http://127.0.0.1:PORT /quote green.json
PRUEBA DE CARGA — escenario cotizar->reservar contra /quote
perfil: smoke(5) -> load(20) -> stress(80) VUs
--------------------------------------------------------------------------
etapa     VUs    reqs      RPS      p50      p95      p99   error   checks
--------------------------------------------------------------------------
smoke       5   23532   5881.7     0.79     1.20     1.42   0.00%  100.00%
load       20   26232   5242.1     3.61     5.94     7.37   0.00%  100.00%
stress     80   31020   5156.6    14.70    25.51    31.42   0.00%  100.00%
--------------------------------------------------------------------------

THRESHOLDS (evaluados sobre la corrida entera — como k6)
--------------------------------------------------------------------------
THRESHOLD                             MEDIDO                RESULTADO
http_req_duration: p(95) < 200ms      p(95) = 21.72ms       PASS
http_req_failed:   rate < 1.00%       rate  = 0.00%         PASS
checks:            rate > 99.00%      rate  = 100.00%       PASS
--------------------------------------------------------------------------
metricas exportadas -> green.json
GATE: PASS  (exit code 0)
$ echo $?
0

El gate rojo (motor de precios pesado, /quote_cpu).

Qué esperar — la misma prueba; bajo el stress el p95 cruza el SLO; el threshold de latencia falla; exit 1. Salida real:

$ python3.14 loadtest.py http://127.0.0.1:PORT /quote_cpu red.json
PRUEBA DE CARGA — escenario cotizar->reservar contra /quote_cpu
perfil: smoke(5) -> load(20) -> stress(80) VUs
--------------------------------------------------------------------------
etapa     VUs    reqs      RPS      p50      p95      p99   error   checks
--------------------------------------------------------------------------
smoke       5    2326    579.7     8.68    14.35    17.06   0.00%  100.00%
load       20    2960    586.9    34.25    57.80    60.93   0.00%  100.00%
stress     80    3522    575.7   138.48   245.24   259.11   0.00%  100.00%
--------------------------------------------------------------------------

THRESHOLDS (evaluados sobre la corrida entera — como k6)
--------------------------------------------------------------------------
THRESHOLD                             MEDIDO                RESULTADO
http_req_duration: p(95) < 200ms      p(95) = 229.19ms      FAIL
http_req_failed:   rate < 1.00%       rate  = 0.00%         PASS
checks:            rate > 99.00%      rate  = 100.00%       PASS
--------------------------------------------------------------------------
metricas exportadas -> red.json
GATE: FAIL  (exit code 1)
$ echo $?
1

Ahí está la prueba de carga entera, en sus dos veredictos. Verde con la carga sana: el build sano cumple el SLO holgado, el gate pasa, el deploy queda autorizado. Rojo cuando el motor de precios se pone pesado: bajo smoke (p95 14.35) y load (p95 57.80) el sistema cumple, pero el stress (p95 245.24) rompe el SLO, el agregado (229.19 ms) cruza los 200, el threshold de latencia falla y el gate bloquea el deploy —con su exit code de verdad—. El error se mantuvo en 0% y los checks en 100% en ambos: la degradación fue de latencia bajo carga, no de disponibilidad ni de corrección. Justo lo que una prueba de stress existe para atrapar.

Y el registro que queda: el green.json exportado de la corrida sana —la libreta que un pipeline subiría como artifact—:

$ cat green.json
{
  "scenario": "quote->book",
  "quote_path": "/quote",
  "slo": { "p95_ms": 200.0, "error_rate": 0.01, "checks_rate": 0.99 },
  "aggregate": {
    "reqs": 80784, "rps": 5378.4,
    "p50": 4.05, "p95": 21.72, "p99": 28.01,
    "error_rate": 0.0, "checks_rate": 1.0
  },
  "stages": [
    { "stage": "smoke",  "vus": 5,  "reqs": 23532, "p95": 1.2,   "error_rate": 0.0, "checks_rate": 1.0 },
    { "stage": "load",   "vus": 20, "reqs": 26232, "p95": 5.94,  "error_rate": 0.0, "checks_rate": 1.0 },
    { "stage": "stress", "vus": 80, "reqs": 31020, "p95": 25.51, "error_rate": 0.0, "checks_rate": 1.0 }
  ],
  "passed": true
}

(Abreviado; el archivo real lleva también rps, p50 y p99 por etapa.) El SLO contra el que se juzgó, las métricas agregadas y por etapa, y el veredicto ("passed": true). Todo lo que necesitas después de cerrar la terminal.

Pieza 4 — El CI (contenido)

La cuarta pieza es el .github/workflows/load.yml de la lección 7: levanta la API, corre k6 con los thresholds como gate (si un threshold falla, k6 run sale con 99 y el job se pone rojo), y sube el results.json como artifact con if: always(). Se dispara nightly, pre-release y on-demand —no en cada PR—. Va como contenido, fiel a GitHub Actions; aquí nunca se ejecuta git/gh. El mecanismo —el exit code que hace fallar el step— lo comprobaste ejecutado en la lección 7 con el mirror local en shell.

La rúbrica de una buena prueba de carga

Tu entrega está completa —y es buena— si cumple estos cuatro criterios. No son de estilo; son lo que separa una prueba que protege de una que decora:

#CriterioQué significa¿En tu entrega?
1Perfil realistaLa carga tiene forma (smoke → load → stress), no un solo nivel plano. Modela el tráfico esperado y lo empuja hasta el pico para ver dónde se dobla. Think time con jitter, no lockstep.
2Thresholds ligados a SLOCada umbral (p95, error, checks) sale de una promesa de negocio concreta, no de un número al azar. El threshold es un pasa/falla, no un adorno.
3Checks de correcciónLa prueba verifica que la respuesta sea correcta bajo carga (status, precio, confirmación), no solo rápida. Un 200 veloz con un precio roto debe fallar.
4En CI, como gateLa prueba corre sola en un pipeline, y el threshold bloquea el deploy con un exit code (no continue-on-error). Se dispara en el ritmo correcto (nightly/pre-release), no en cada PR.

Repasa tu entrega contra la rúbrica:

  • Perfil realista ✓ — stages smoke→load→stress→ramp-down, con datos parametrizados (salas/planes/horas variables) y sleep con jitter. La corrida ejecutada mostró el p95 subir con la carga (14 → 58 → 245 ms), justo lo que un perfil con forma revela.
  • Thresholds ligados a SLO ✓ — p(95)<200 (la promesa de latencia), rate<0.01 (la de disponibilidad), checks>0.99 (la de corrección). En la lección 4 viste que el mismo p95 pasa o falla según el SLO: el umbral es la promesa, no un capricho.
  • Checks de corrección ✓ — cinco check() por iteración (status 200, precio correcto, reserva confirmada, booking_id presente). Se mantuvieron en 100% bajo carga, confirmando que Reservo no solo respondió rápido sino bien.
  • En CI, como gate ✓ — el load.yml corre k6 con los thresholds como gate; el exit code (99 en k6, 1 en el gate de Python) bloquea el deploy, como comprobaste con el mirror local. Disparado nightly/pre-release/on-demand.

Si los cuatro están, tu prueba de carga no solo mide: protege. Ese era el objetivo de toda la guía.

Qué haces cuando el gate se pone rojo (la frontera)

La prueba te dejó parado ante un gate rojo: el p95 de /quote_cpu cruzó el SLO bajo carga. ¿Y ahora? La prueba de carga te dice que hay un problema de rendimiento y dónde mirar (el motor de precios, saturado de CPU bajo concurrencia), pero arreglarlo —perfilar la app, encontrar la función lenta, optimizar la consulta, poner un índice, meter caché— es el "después", y está fuera de esta guía. Es un trabajo distinto: la prueba de carga encuentra el cuello de botella; la optimización lo resuelve. El gate rojo es el principio de esa conversación, no el final. Con lo que sabes ahora, sabrías reconocer el síntoma (RPS plano + p95 creciente = saturación de throughput) y por dónde empezar a investigar; profundizar en la optimización es el paso siguiente en tu camino.

El arco de la guía: los ocho módulos

Cierras aquí un recorrido completo. Vale la pena verlo entero, porque cada módulo fue un eslabón y ahora tienes la cadena:

  1. Por qué (M1). La diferencia entre "¿funciona?" (corrección) y "¿aguanta?" (carga); los tipos de prueba (smoke/load/stress/spike/soak); qué es k6 y su lugar. La pregunta que todo lo demás responde.
  2. El script y los VUs (M2). La anatomía de un script k6 —la función default, http.post, check, sleep, options— y el modelo del usuario virtual. El qué hace cada usuario.
  3. Las métricas (M3). La latencia y por qué el promedio miente frente a los percentiles (p95/p99); el throughput/RPS; la tasa de error. Los instrumentos con los que se lee todo.
  4. Los perfiles (M4). Los stages y las tres fases (ramp-up/steady/ramp-down); las formas (constante, rampa, spike); los executors. El cuántos usuarios y cuándo.
  5. Los thresholds (M5). El umbral que convierte una métrica en un pasa/falla, el exit code que hace fallar el CI, y el SLO de donde sale el número. El veredicto.
  6. Los checks y escenarios (M6). Verificar la corrección bajo carga con check(), la correlación, los datos parametrizados y el think time realista. El ¿respondió bien, no solo rápido?
  7. El análisis y el CI (M7). Leer y exportar el resultado, detectar una regresión, y automatizar la prueba en un pipeline con el threshold como gate. El ¿qué haces con el resultado y cómo lo automatizas?
  8. La prueba completa (M8). Ensamblar todo lo anterior en una prueba de carga entera de Reservo, con su gate verde y rojo, en CI, y una rúbrica. La integración.

De "¿por qué probar la carga?" a "aquí está la prueba completa, en un pipeline, bloqueando el deploy si el rendimiento se degrada". Ese es el arco, y lo recorriste entero.

A dónde seguir

Con la carga dominada, hay dos direcciones naturales:

  • La otra mitad de la pirámide: la corrección. Esta guía probó que Reservo aguanta; la corrección de lo que el usuario ve es otra pregunta, y vive en las guías hermanas del ecosistema de testing. e2e-testing-with-playwright-guide prueba el flujo de Reservo por el navegador (que un usuario que elige Focus/basic/3h ve $75.00 en la pantalla) —la CORRECCIÓN de la UI, no la carga—. Y testing-fundamentals-and-tdd-guide cubre la base de la pirámide: los tests unitarios y el TDD que sostienen todo lo demás. Carga (aquí), E2E (Playwright) y unitarios/TDD (fundamentals) son las tres capas que un sistema serio prueba.
  • El "después" de un gate rojo: la optimización. Cuando un threshold falla, la prueba te dijo dónde mirar; el siguiente trabajo es arreglarlo —perfilar la aplicación y la base de datos, encontrar el cuello de botella (una consulta N+1, un índice faltante, un cálculo caro), y optimizarlo—. Eso está fuera de esta guía, pero ahora sabes reconocer cuándo hace falta y por dónde empezar: un p95 que cruza el SLO bajo carga, con el RPS estancado, apuntando a una saturación que hay que resolver en la app.

Resumen y cierre de la guía

En esta última lección entregaste la prueba de carga completa de Reservo: el script k6 (contenido) con el escenario cotizar→reservar, los check(), el sleep con jitter, los stages smoke→load→stress y los thresholds ligados al SLO; la API canónica como blanco; la corrida ejecutable en Python con su gate verde (build sano /quote, p95 agregado 21.72 ms, exit 0) y rojo (motor pesado /quote_cpu, el stress lleva el p95 a 245.24 ms, agregado 229.19 ms, exit 1), con el results.json exportado; y el load.yml de CI (contenido) con el threshold como gate. Y la juzgaste con la rúbrica de una buena prueba de carga —perfil realista, thresholds ligados a SLO, checks de corrección, en CI—, que tu entrega cumple en las cuatro.

Con esto cierras la guía. Empezaste preguntando "¿aguanta bajo carga?" y terminas con una prueba entera que responde esa pregunta sola, en un pipeline, bloqueando el deploy si el rendimiento se degrada. Sabes diseñar el escenario, moldear la carga, elegir los umbrales desde el SLO, verificar la corrección bajo estrés, leer las métricas con criterio, y automatizar todo con un gate. La prueba de carga dejó de ser una caja negra: es una herramienta que entiendes de punta a punta y que puedes construir para cualquier API. Lo que sigue —la corrección por E2E, la base de TDD, la optimización del "después"— son caminos que ahora sabes por dónde tomar. Buen viaje.

Recursos

  • k6 — Get started (el script completo) — el recorrido oficial que arma un script con http, check, sleep, options, stages y thresholds, como el quote_book_test.js que entregaste. La referencia del script del capstone.
  • e2e-testing-with-playwright-guide — la guía hermana que prueba la corrección del flujo de Reservo por el navegador; la otra mitad de la pirámide, a donde sigues para cubrir lo que la carga no prueba.
  • testing-fundamentals-and-tdd-guide — la base de la pirámide: los tests unitarios y el TDD que sostienen el E2E y la carga. El fundamento sobre el que todo lo demás se apoya.
  • Google SRE Book — Service Level Objectives — el criterio con el que se eligen los thresholds del capstone y con el que se decide si el rendimiento medido "es bueno"; la brújula del "después" cuando el gate se pone rojo.