Módulo 6: Checks, groups y escenarios realistas

8. Mini-proyecto: un escenario realista de Reservo

Descripción

Esta lección es tu graduación del módulo 6. A lo largo de siete lecciones montaste, pieza por pieza, todo lo que hace que una prueba de carga se parezca al uso real: el check de corrección (verificar status, valor y forma), el group() (organizar y medir por paso), la parametrización (variar el dato para no golpear una ruta caliente), la correlación (extraer un valor y reusarlo en el paso siguiente) y el think time con jitter (el ritmo humano). Ahora las juntas todas en un solo entregable de principio a fin: un escenario realista de Reservo —cotizar → reservar → confirmar— parametrizado, con checks en cada paso y el booking_id correlacionado de verdad, corrido contra la API canónica, más su equivalente en k6 como contenido.

El entregable tiene cuatro partes, y las cuatro importan: (1) el script de k6 completo del escenario (contenido), con group, check, SharedArray y la correlación; (2) la API canónica con el endpoint GET /booking/<id> declarado en este módulo; (3) el generador de Python que corre el escenario de tres pasos y su corrida real contra la API, reportando la tasa de checks y las métricas por grupo; y (4) una reflexión sobre el mapeo —qué pieza de k6 corresponde a qué línea de Python, y cómo las cinco piezas del módulo aparecen en el escenario—. Esa cuarta parte amarra lo que escribiste con lo que mediste: el punto del módulo no es tener un script, sino entender el escenario que describe.

Conexión con el módulo: aquí se cierra el arco. Las lecciones 2 a 7 te dieron las piezas; esta te pone a producir el escenario completo con tus manos, como lo harías el primer día que escribes una prueba de carga realista para una API de verdad. El script de k6 va como contenido rotulado (correcto, no ejecutado aquí); el generador de Python se ejecuta de verdad, y su salida —8 VUs, 252 iteraciones, 100 % de checks, 2016 de 2016— fue medida en este entorno con Python 3.14.0 contra la API canónica. Cuando termines, entras al módulo 7 —analizar resultados y correr en CI— con un escenario realista ya construido y medido.

El ensayo general antes del estreno

Piénsalo así. Antes de estrenar una obra de teatro, la compañía hace un ensayo general: la función completa, de principio a fin, con vestuario, luces y utilería, exactamente como será el estreno. No se ensaya una escena suelta ni se recita el guion sentados; se corre la obra entera, en orden, para ver si las piezas encajan cuando se tocan juntas —si la actriz llega a tiempo a su marca, si el cambio de escena fluye, si el diálogo del segundo acto se apoya bien en lo que pasó en el primero—. El ensayo general es donde se descubre lo que los ensayos por partes no revelan: los problemas de integración, los que solo aparecen cuando todo corre junto.

Tu mini-proyecto es ese ensayo general. En las lecciones anteriores ensayaste cada pieza por separado —los checks en la 2, los groups en la 4, la parametrización en la 5, la correlación en la 6, el think time en la 7—. Ahora corres la obra entera: un escenario donde un VU cotiza una sala variada (parametrización), verifica el precio (check de corrección), toma ese precio y reserva (correlación 1), verifica la reserva (check), toma el id y confirma (correlación 2), verifica la confirmación (check), todo organizado en pasos (groups) y con pausas realistas (think time con jitter). Correrlo completo es lo que prueba que las piezas encajan cuando se tocan juntas —que la cadena de custodia aguanta bajo carga, con datos variados, en las 252 iteraciones—.

El mini-proyecto es el ensayo general: correr el escenario realista completo —parametrización + checks + correlación + groups + think time— de principio a fin, en orden, bajo carga. No es ensayar una pieza suelta, es ver que todas encajan cuando se tocan juntas. Eso es lo que prueba que tienes un escenario, no una colección de peticiones.

Lo que vas a construir

Un mini-proyecto con tres archivos: la API canónica (el blanco), el script de k6 (contenido) y el generador de Python (ejecutado).

reservo-scenario/
├── reservo_api.py         # la API canonica + GET /booking/<id> (el blanco de carga)
├── scenario.js            # el script de k6 del escenario (CONTENIDO: k6 no esta instalado)
└── scenario_loadgen.py    # el generador Python que corre el escenario (SE EJECUTA)

Sigue los pasos en orden; cada uno se apoya en el anterior.

Paso 1 — La API canónica con GET /booking/<id>

Es el servidor de Reservo, con los endpoints canónicos (/rooms, /quote, /book) más el GET /booking/<id> declarado en este módulo, que guarda cada reserva y la devuelve por su id. Usa puerto 0 (el sistema operativo asigna un puerto libre) y request_queue_size alto para aguantar ráfagas:

# reservo_api.py (fragmento clave) - la API canonica + GET /booking/<id>.
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import json, itertools, threading

ROOM_RATES_CENTS = {"Focus": 2500, "Studio": 4000, "Boardroom": 8000}
VALID_TIERS = {"basic", "pro"}
_booking_counter = itertools.count(1)
_bookings = {}                          # store en memoria: booking_id -> reserva
_store_lock = threading.Lock()

def price_cents(room, tier, hours):
    total = ROOM_RATES_CENTS[room] * hours
    return total * 80 // 100 if tier == "pro" else total   # descuento pro entero

class ReservoHandler(BaseHTTPRequestHandler):
    protocol_version = "HTTP/1.1"
    def log_message(self, *a): pass

    def _send_json(self, status, payload):
        body = json.dumps(payload).encode()
        self.send_response(status)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers(); self.wfile.write(body)

    def do_GET(self):
        if self.path == "/rooms":
            rooms = [{"room": n, "rate_cents": r} for n, r in ROOM_RATES_CENTS.items()]
            return self._send_json(200, {"rooms": rooms})
        if self.path.startswith("/booking/"):          # GET /booking/<id>: consultar reserva
            bid = self.path[len("/booking/"):]
            with _store_lock:
                booking = _bookings.get(bid)
            if booking is None:
                return self._send_json(404, {"error": "booking_not_found"})
            return self._send_json(200, booking)
        return self._send_json(404, {"error": "not_found"})

    def do_POST(self):
        length = int(self.headers.get("Content-Length", 0))
        raw = self.rfile.read(length) if length else b"{}"
        try:
            data = json.loads(raw or b"{}")
        except json.JSONDecodeError:
            return self._send_json(400, {"error": "invalid_json"})
        room, tier, hours = data.get("room"), data.get("tier"), data.get("hours")
        if room not in ROOM_RATES_CENTS or tier not in VALID_TIERS:
            return self._send_json(400, {"error": "invalid_room_or_tier"})
        if not isinstance(hours, int) or not (1 <= hours <= 12):
            return self._send_json(400, {"error": "invalid_hours"})
        cents = price_cents(room, tier, hours)
        if self.path == "/quote":
            return self._send_json(200, {"price_cents": cents})
        if self.path == "/book":
            quoted = data.get("price_cents")            # correlacion: precio cotizado
            if quoted is not None and quoted != cents:
                return self._send_json(409, {"error": "price_mismatch", "price_cents": cents})
            bid = f"bk-{next(_booking_counter):06d}"
            booking = {"booking_id": bid, "confirmed": True, "room": room,
                       "tier": tier, "hours": hours, "price_cents": cents}
            with _store_lock:
                _bookings[bid] = booking
            return self._send_json(200, booking)
        return self._send_json(404, {"error": "not_found"})

def main():
    ThreadingHTTPServer.request_queue_size = 512        # aguanta rafagas de conexiones
    server = ThreadingHTTPServer(("127.0.0.1", 0), ReservoHandler)  # puerto 0
    _, port = server.server_address
    with open("port.txt", "w") as f:
        f.write(str(port))
    print(f"Reservo API en http://127.0.0.1:{port}")
    server.serve_forever()

if __name__ == "__main__":
    main()

Arráncala en una terminal —python3 reservo_api.py— y déjala corriendo. Escribirá el puerto asignado en port.txt, que el generador leerá.

Paso 2 — El script de k6 del escenario (contenido)

Este es el entregable de k6: el escenario completo con las cinco piezas del módulo —SharedArray (parametrización), group (organización), check (corrección), correlación (precio y id) y sleep con jitter (think time)—. Se muestra como contenido, correcto y verificado contra la documentación de k6, pero k6 no está instalado aquí:

// scenario.js - escenario realista cotizar -> reservar -> confirmar.
// MOSTRADO COMO CONTENIDO: k6 no esta instalado en este entorno.
// Se correria con:  k6 run scenario.js
import http from 'k6/http';
import { check, group, sleep } from 'k6';
import { SharedArray } from 'k6/data';

export const options = {
  vus: 8,
  duration: '5s',
  thresholds: {
    checks: ['rate>0.99'],              // el veredicto (M5): >99% de checks deben pasar
  },
};

// (Parametrizacion) el dataset, cargado una vez y compartido.
const dataset = new SharedArray('reservations', function () {
  return [
    { room: 'Focus',     tier: 'basic', hours: 3, expected: 7500 },
    { room: 'Focus',     tier: 'pro',   hours: 3, expected: 6000 },
    { room: 'Studio',    tier: 'basic', hours: 2, expected: 8000 },
    { room: 'Studio',    tier: 'pro',   hours: 4, expected: 12800 },
    { room: 'Boardroom', tier: 'basic', hours: 1, expected: 8000 },
    { room: 'Boardroom', tier: 'pro',   hours: 6, expected: 38400 },
  ];
});

const BASE_URL = 'http://localhost:8000';
const params = { headers: { 'Content-Type': 'application/json' } };

function thinkTime(min, max) {           // (think time) pausa con jitter
  return Math.random() * (max - min) + min;
}

export default function () {
  const row = dataset[Math.floor(Math.random() * dataset.length)];  // dato parametrizado
  let price, bookingId;

  // (group + check) Paso 1: cotizar
  group('quote', function () {
    const body = JSON.stringify({ room: row.room, tier: row.tier, hours: row.hours });
    const res = http.post(`${BASE_URL}/quote`, body, params);
    check(res, {
      'status is 200': (r) => r.status === 200,
      'price is correct': (r) => r.json('price_cents') === row.expected,
    });
    price = res.json('price_cents');     // (correlacion 1) extrae el precio
  });

  sleep(thinkTime(0.5, 1.5));            // think time con jitter entre pasos

  // (group + check + correlacion) Paso 2: reservar
  group('book', function () {
    const body = JSON.stringify({ room: row.room, tier: row.tier,
                                  hours: row.hours, price_cents: price });  // usa el precio
    const res = http.post(`${BASE_URL}/book`, body, params);
    check(res, {
      'status is 200': (r) => r.status === 200,
      'booking is confirmed': (r) => r.json('confirmed') === true,
      'booking_id present': (r) => String(r.json('booking_id')).startsWith('bk-'),
    });
    bookingId = res.json('booking_id');  // (correlacion 2) extrae el id
  });

  sleep(thinkTime(0.5, 1.5));

  // (group + check + correlacion) Paso 3: confirmar
  group('confirm', function () {
    const res = http.get(`${BASE_URL}/booking/${bookingId}`);   // usa el id
    check(res, {
      'status is 200': (r) => r.status === 200,
      'id matches': (r) => r.json('booking_id') === bookingId,
      'price matches quote': (r) => r.json('price_cents') === price,
    });
  });
}

Las cinco piezas del módulo, en un archivo: SharedArray (lección 5), group (lección 4), check (lección 2), la correlación del precio y el id (lección 6), y sleep con jitter (lección 7). Más el threshold sobre checks (módulo 5, reusado en la lección 3) que da el veredicto. Si tuvieras k6 instalado, lo correrías con k6 run scenario.js.

Paso 3 — El generador de Python (el que se ejecuta)

Y este es el entregable que sí se ejecuta: el generador que corre el escenario de tres pasos con ThreadPoolExecutor (un worker por VU), datos parametrizados, correlación real del precio y el id, checks en cada paso y think time con jitter. Su estructura es la que viste en las lecciones 5, 6 y 7:

# scenario_loadgen.py (nucleo) - una iteracion del escenario cotizar->reservar->confirmar.
def iteration(rng):
    row = rng.choice(DATASET)                       # (parametrizacion) dato variado
    room, tier, hours = row["room"], row["tier"], row["hours"]
    want = expected_price(room, tier, hours)

    # ---- group: quote ----  (check de correccion + extrae price)
    s1, b1 = post_json(f"{BASE_URL}/quote", {"room": room, "tier": tier, "hours": hours})
    price = b1.get("price_cents")
    record("quote: status is 200", s1 == 200)
    record("quote: price is correct", price == want)

    # ---- group: book ----  (correlacion 1: price -> book; extrae booking_id)
    s2, b2 = post_json(f"{BASE_URL}/book",
                       {"room": room, "tier": tier, "hours": hours, "price_cents": price})
    booking_id = b2.get("booking_id")
    record("book: status is 200", s2 == 200)
    record("book: confirmed is true", b2.get("confirmed") is True)
    record("book: booking_id present", bool(booking_id) and str(booking_id).startswith("bk-"))

    # ---- group: confirm ----  (correlacion 2: booking_id -> URL)
    s3, b3 = get_json(f"{BASE_URL}/booking/{booking_id}")
    record("confirm: status is 200", s3 == 200)
    record("confirm: id matches", b3.get("booking_id") == booking_id)
    record("confirm: price matches quote", b3.get("price_cents") == price)

    think(rng, THINK_MS)                             # (think time con jitter)

Con el servidor corriendo (Paso 1) y su puerto en port.txt, córrelo con 8 VUs, 5 segundos y think time de 150 ms:

python3 scenario_loadgen.py "http://127.0.0.1:$(cat port.txt)" 8 5 150

Qué esperar. Con think time de 150 ms (con jitter), 8 VUs deben producir del orden de cientos de iteraciones, con 8 checks por iteración (2 de cotizar + 3 de reservar + 3 de confirmar), todos en verde si la cadena de custodia aguanta. Salida real en este entorno:

----------------------------------------------------------------
  Escenario cotizar->reservar->confirmar  ->  8 VUs / 5s / think 150ms
----------------------------------------------------------------
  vus............: 8
  duracion real..: 5.18s
  iterations.....: 252   (48.6/s)
  checks.........: 100.00%   (2016 de 2016)
    quote: status is 200..............:  252 ok / 0    fail
    quote: price is correct...........:  252 ok / 0    fail
    book: status is 200...............:  252 ok / 0    fail
    book: confirmed is true...........:  252 ok / 0    fail
    book: booking_id present..........:  252 ok / 0    fail
    confirm: status is 200............:  252 ok / 0    fail
    confirm: id matches...............:  252 ok / 0    fail
    confirm: price matches quote......:  252 ok / 0    fail
  http_errors....: 0
  por grupo (latencia media, n llamadas):
    group 'quote  '.....: n=252  avg=1.35ms
    group 'book   '.....: n=252  avg=0.78ms
    group 'confirm'.....: n=252  avg=0.63ms

Léela como el ensayo general que salió bien:

  • iterations: 252 (48.6/s) — 8 VUs concurrentes completaron 252 recorridos de tres pasos en ~5 segundos, con un think time realista de 150 ms (más el jitter). Cada recorrido con un dato posiblemente distinto del dataset.
  • checks: 100.00% (2016 de 2016) — 8 checks por iteración × 252 iteraciones = 2016 checks, todos verdes. La obra entera se cumplió: cada cotización dio el precio correcto, cada reserva se confirmó con un booking_id real, y cada confirmación encontró ese id con ese precio.
  • confirm: id matches: 252 ok / 0 fail y confirm: price matches quote: 252 ok / 0 fail — la prueba de que la correlación aguantó bajo carga. En las 252 iteraciones, con 8 VUs pisándose, cada una extrajo su propio booking_id (todos distintos) y su propio precio, y los reusó correctamente. La cadena de custodia no se cruzó ni se rompió ni una vez.
  • El desglose por grupo (quote 1.35 ms, book 0.78 ms, confirm 0.63 ms, 252 cada uno) confirma que los tres pasos corrieron en cada iteración, con la cotización como la más pesada (lección 4).
  • http_errors: 0 — con request_queue_size alto y 8 VUs, el servidor aguantó todas las conexiones. (Bajo más carga podrían aparecer errores de conexión, y eso sería dato real, como viste en el mini-proyecto del módulo 2.)

Paso 4 — La reflexión: las cinco piezas en el escenario

La cuarta parte del entregable es escribir cómo las cinco piezas del módulo aparecen en el escenario, y cómo se corresponden las dos caras (k6 y Python). Esta tabla es la guía:

Pieza del móduloEn scenario.js (k6, contenido)En scenario_loadgen.py (Python, ejecutado)
Parametrización (L5)SharedArray + dataset[Math.floor(...)]DATASET + rng.choice(DATASET)
Group (L4)group('quote', () => {...})group_calls["quote"] (latencia por grupo)
Check de corrección (L2)check(res, { 'price is correct': ... })record("quote: price is correct", price == want)
Correlación 1: precio (L6)price = res.json('price_cents') → body de /bookprice = b1.get("price_cents") → body de /book
Correlación 2: id (L6)bookingId = res.json('booking_id') → URLbooking_id = b2.get("booking_id") → URL
Think time con jitter (L7)sleep(thinkTime(0.5, 1.5))think(rng, THINK_MS) con uniform(0.5, 1.5)
Veredicto (M5, L3)thresholds: { checks: ['rate>0.99'] }un gate sobre la tasa de checks (exit 0/1)

Y anota las diferencias honestas: el resumen de k6 traería percentiles (p(95)) y un group_duration con avg/min/max/percentiles por grupo, mientras el generador de Python reporta la latencia media por grupo (las métricas a fondo fueron el módulo 3). Ambos corren el mismo escenario contra la misma API; el de k6 lo instrumenta con más métricas. Lo esencial —parametrización, checks, correlación, groups, think time— es idéntico en las dos caras.

Rúbrica de tu mini-proyecto

Tu entregable está completo si cumple estas cuatro partes:

  • (1) El script de k6 (contenido). ¿Tiene las cinco piezas —SharedArray, group por paso, check de corrección, correlación del precio y del id, sleep con jitter— correctas y en su lugar? ¿La correlación extrae el valor de una respuesta y lo reusa en la siguiente (precio en el body de /book, id en la URL de /booking/<id>)? ¿Hay un threshold sobre checks que dé el veredicto?
  • (2) La API canónica. ¿Corre con los endpoints canónicos (/rooms, /quote, /book) más el GET /booking/<id> declarado? ¿/quote devuelve 7500 para Focus/basic/3h y 6000 para Focus/pro/3h? ¿/book guarda la reserva y /booking/<id> la devuelve?
  • (3) El generador de Python (ejecutado). ¿Corre el escenario de tres pasos contra la API canónica y produce una salida real con la tasa de checks, el desglose por criterio y las métricas por grupo? ¿Parametriza el dato (elige del dataset), correlaciona el precio y el id de verdad, y hace checks en cada paso? ¿Los checks id matches y price matches quote están en verde (prueba de que la correlación funcionó)?
  • (4) La reflexión. ¿Puedes ubicar las cinco piezas del módulo en el escenario (parametrización, group, check, correlación, think time) y mapear al menos cinco entre las dos caras? ¿Nombras una diferencia honesta entre los resúmenes?

Si las cuatro están, dominas la construcción de un escenario realista —el objetivo entero del módulo—.

Errores comunes

Correr el generador sin arrancar la API primero (o con un puerto viejo). Qué pasa: se corre scenario_loadgen.py sin reservo_api.py arriba, o apuntando a un puerto de una corrida anterior (recuerda que puerto 0 cambia en cada arranque). Cómo detectarlo: http_errors altísimo y checks en 0 %. Cómo corregirlo: arranca la API en una terminal, déjala corriendo, y en otra corre el generador leyendo port.txt en el mismo momento ($(cat port.txt)). Servidor arriba, generador después.

Un id matches en rojo: la correlación se rompió. Qué pasa: el escenario corre pero el check confirm: id matches (o status is 200 del paso confirmar) falla en muchas iteraciones. Por qué pasa: el booking_id no se está extrayendo bien —quizás se hardcodeó, o se leyó de la respuesta equivocada, o se guardó dentro del grupo y se perdió—. Cómo detectarlo: el paso confirmar da 404 o id que no coincide. Cómo corregirlo: verifica que extraes el id de la respuesta de /book (b2.get("booking_id")) y lo usas en la URL del paso 3. Es la correlación de la lección 6; el check en verde es su prueba.

Reportar solo "252 iteraciones" sin la tasa de checks ni el think time. Qué pasa: alguien resume su corrida con las iteraciones y ya. Por qué pasa: la iteración se siente como "el resultado". Cómo detectarlo: si tu reporte no dice cuántos checks pasaron ni con qué think time corriste, le falta lo esencial. Cómo corregirlo: reporta la tasa de checks (100 %, 2016/2016), el desglose por paso, las métricas por grupo, y el contexto (VUs y think time). Un escenario se juzga por si hizo lo correcto (checks) y a qué ritmo (VUs + think time), no solo por cuántas veces corrió.

Ejercicios

Ejercicio 1 — Cambia el reparto y predice. Vas a correr el generador con 4 VUs durante 4 segundos y think time de 300 ms. (a) ¿Más o menos iteraciones que la corrida de 8 VUs / 5 s / 150 ms, y por qué? (b) ¿Cuántos checks por iteración esperas y por qué? (c) ¿Esperas que id matches siga en verde?

Ver solución
  • (a) Bastantes menos. Hay la mitad de VUs (4 vs 8), menos tiempo (4 s vs 5 s) y el doble de think time (300 ms vs 150 ms), que espacia más las iteraciones. Los tres factores reducen las iteraciones. (El comando sería python3 scenario_loadgen.py "http://127.0.0.1:$(cat port.txt)" 4 4 300.)
  • (b) 8 checks por iteración: 2 del paso cotizar (status, precio), 3 del de reservar (status, confirmado, id presente) y 3 del de confirmar (status, id coincide, precio coincide).
  • (c) Sí. La correlación no depende de la carga: cada iteración extrae su propio booking_id y lo reusa. Mientras la API esté sana y el id se extraiga bien, id matches queda en verde con cualquier número de VUs.

Ejercicio 2 — Ubica las cinco piezas. En el script scenario.js, señala la línea (o construcción) que corresponde a cada una de las cinco piezas del módulo: (a) parametrización, (b) group, (c) check de corrección, (d) correlación del id, (e) think time con jitter.

Ver solución
  • (a) Parametrización: const dataset = new SharedArray(...) y const row = dataset[Math.floor(Math.random() * dataset.length)].
  • (b) Group: group('quote', function () {...}) (y sus hermanos group('book', ...), group('confirm', ...)).
  • (c) Check de corrección: cualquier check(res, { 'price is correct': (r) => r.json('price_cents') === row.expected }).
  • (d) Correlación del id: bookingId = res.json('booking_id') en el paso de reservar, reusado en `${BASE_URL}/booking/${bookingId}` en el de confirmar.
  • (e) Think time con jitter: sleep(thinkTime(0.5, 1.5)) entre los grupos.

Ejercicio 3 — Interpreta el ensayo general. La corrida real dio checks: 100.00% (2016 de 2016) con http_errors: 0 y el desglose por grupo (quote 1.35 ms, book 0.78 ms, confirm 0.63 ms). Escribe, en tres o cuatro frases, el veredicto que le darías a esta prueba: ¿qué probó, qué no probó, y qué harías después?

Ver solución

Un veredicto razonable: la prueba pasó el ensayo general. Con 8 VUs concurrentes y datos parametrizados, el escenario cotizar → reservar → confirmar se cumplió en las 252 iteraciones con 100 % de checks —el precio siempre correcto, la reserva siempre confirmada, la correlación del booking_id intacta (id y precio coinciden en las 252)— y sin errores de conexión. Las piezas encajan cuando se tocan juntas.

Lo que no probó: cómo se comporta bajo carga alta (8 VUs es poco; con un perfil de rampa a cientos de VUs —módulo 4— podrían aparecer errores de conexión o latencias en cola), ni fija un veredicto automático (para eso hace falta el threshold sobre checks corriendo en un pipeline). Qué haría después: subir la carga con stages (M4), poner el threshold como gate, y llevarlo a CI para que corra en cada deploy (M7). Este escenario realista es la base sobre la que se montan esos pasos.

Resumen y siguiente paso

En este mini-proyecto juntaste todo el módulo en un entregable de cuatro partes: un script de k6 del escenario (contenido, con las cinco piezas —SharedArray, group, check, correlación del precio y el id, sleep con jitter— más el threshold del veredicto), la API canónica con el GET /booking/<id> declarado, un generador de Python que corre el escenario de tres pasos y su corrida real contra la API, y una reflexión sobre el mapeo. La corrida real —8 VUs, 252 iteraciones, 100 % de checks (2016/2016), 0 errores— fue el ensayo general que salió bien: con id matches y price matches quote en verde en las 252 iteraciones, probó que la cadena de custodia de la correlación aguanta bajo carga, con datos variados, cuando todas las piezas se tocan juntas.

Con esto cierras el módulo 6. Sabes construir una prueba de carga que se parece al uso real: verificar la corrección (no solo la respuesta) con checks, organizar los pasos con groups, variar el dato con parametrización para no golpear una ruta caliente, encadenar los pasos con correlación (cotizar → reservar → confirmar), y darle ritmo humano con think time y jitter. La colección de peticiones sueltas se volvió un escenario.

Lo que sigue, el módulo 7, es el "después" de tener un escenario que corre: analizar sus resultados y llevarlo a CI. Aprenderás a leer la tendencia de una corrida, a exportar las métricas (--out json), a detectar una regresión de rendimiento comparando dos corridas, y a montar el escenario en un pipeline de GitHub Actions (contenido) con el threshold como gate que falla el deploy. El escenario realista que construiste aquí es justo lo que ese pipeline correrá.

Recursos