Módulo 6: Checks, groups y escenarios realistas

6. Correlación: extraer y reusar (cotizar → reservar)

Descripción

Llegamos al corazón del módulo. Hasta ahora, aunque el escenario tiene varios pasos y el dato varía, cada paso es independiente: tú fijas los datos de cotizar, tú fijas los de reservar, tú fijas los de confirmar. Pero un flujo real no funciona así. En un flujo real, cada paso usa lo que le devolvió el anterior. Cotizas una sala y la API te responde un precio; cuando reservas, mandas ese precio, no uno que inventaste. La reserva te devuelve un identificador; cuando confirmas, usas ese identificador, no uno cualquiera. Los pasos están encadenados por los datos que fluyen entre ellos. Reproducir esa cadena en una prueba de carga es la correlación, y es lo que separa un escenario de verdad de una lista de peticiones sueltas.

Correlación es una palabra técnica para algo simple: extraer un valor de una respuesta y usarlo en la petición siguiente. Cotizas → extraes el price_cents de la respuesta → lo mandas en la reserva. Reservas → extraes el booking_id de la respuesta → lo usas en la URL para confirmar. El valor no lo inventas ni lo hardcodeas: lo sacas de lo que la API te acaba de devolver. Y aquí está lo profundo: un valor correlacionado es dinámico —cambia en cada iteración, porque cada reserva genera un booking_id nuevo—. Si intentaras hardcodear el id, funcionaría para una iteración y fallaría en todas las demás. La correlación es la única forma de encadenar pasos cuando el enlace entre ellos es un valor que el servidor genera al vuelo.

Conexión con el módulo: la correlación es la cuarta y central pieza del escenario realista. Junta todo lo anterior: los checks (lección 2) verifican cada paso, los groups (lección 4) los organizan, la parametrización (lección 5) varía el dato inicial, y la correlación los encadena. El flujo cotizar → reservar → confirmar se ejecuta de verdad en Python contra la API canónica, con el price_cents y el booking_id correlacionados realmente; el patrón de k6 —res.json('booking_id') y usarlo en la siguiente petición— va como contenido. Verás la cadena completa medida en este entorno con Python 3.14.0, incluida la prueba de que el booking_id extraído es el que funciona en el paso siguiente.

La cadena de custodia

Piénsalo como una cadena de custodia, la que se usa con una pieza de evidencia o un paquete que pasa de mano en mano. En la recepción de un edificio, llega un paquete y el guardia le pone una etiqueta con un número de seguimiento que él genera en ese momento —no existía antes—. Cuando el paquete sube al piso 5, el mensajero no inventa un número: usa el que el guardia puso. Cuando el destinatario firma, firma contra ese mismo número. Cada eslabón de la cadena usa el identificador que le pasó el eslabón anterior; el número nació en el primer paso y viaja intacto hasta el final. Si alguien en el medio usara un número inventado, la cadena se rompería: el sistema no encontraría ese paquete.

Un flujo cotizar → reservar → confirmar es esa cadena de custodia. La cotización genera un dato (el price_cents) que la reserva usa. La reserva genera un identificador (el booking_id) que la confirmación usa —un número que nace en el paso de reservar y no existía antes—. Correlación es respetar la cadena: en cada paso, usar el valor que te pasó el paso anterior, no uno inventado. Y como el booking_id nace en tiempo de ejecución (cada reserva genera uno nuevo), la única forma de tenerlo es extraerlo de la respuesta —igual que el mensajero solo puede leer el número que el guardia acaba de escribir, no adivinarlo—.

Correlación es extraer un valor de una respuesta y usarlo en la petición siguiente, en vez de inventarlo o hardcodearlo. Es la cadena de custodia del escenario: el price_cents que te cotizaron viaja a la reserva; el booking_id que la reserva generó viaja a la confirmación. Como ese id nace en tiempo de ejecución y es distinto en cada iteración, extraerlo de la respuesta es la única forma de encadenar el flujo.

La correlación en k6 (contenido)

En k6, extraer un valor de una respuesta es leer su cuerpo con res.json('clave'); usarlo en el paso siguiente es meterlo en el body o en la URL de la próxima petición. Aquí está el flujo de tres pasos con las dos correlaciones —el precio y el id— marcadas:

// scenario_correlated.js - cotizar -> reservar -> confirmar con correlacion.
// MOSTRADO COMO CONTENIDO: k6 no esta instalado en este entorno.
import http from 'k6/http';
import { check, group, sleep } from 'k6';

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

export default function () {
  // --- Paso 1: cotizar ---  (produce price_cents)
  let price;
  group('quote', function () {
    const body = JSON.stringify({ room: 'Studio', tier: 'pro', hours: 4 });
    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') === 12800,
    });
    price = res.json('price_cents');          // EXTRAE el precio de la respuesta
  });

  sleep(1);

  // --- Paso 2: reservar ---  (usa price_cents; produce booking_id)
  let bookingId;
  group('book', function () {
    // CORRELACION 1: el price_cents cotizado viaja a la reserva.
    const body = JSON.stringify({ room: 'Studio', tier: 'pro', hours: 4, price_cents: price });
    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');       // EXTRAE el id de la respuesta
  });

  sleep(1);

  // --- Paso 3: confirmar ---  (usa booking_id)
  group('confirm', function () {
    // CORRELACION 2: el booking_id generado viaja a la URL.
    const res = http.get(`${BASE_URL}/booking/${bookingId}`);
    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 dos correlaciones, marcadas:

  • price = res.json('price_cents') (fin del paso 1) price_cents: price (body del paso 2). El precio que la cotización devolvió se extrae y se reusa en la reserva. En una API real, reenviar ese precio le permitiría al backend verificar que la cotización sigue vigente y responder 409 si cambió; nuestro servidor de laboratorio no lo valida (la reserva solo confirma), pero el patrón de reusar el precio cotizado —en vez de recalcularlo o inventarlo— es el mismo. En k6, la variable price guarda el valor entre grupos.
  • bookingId = res.json('booking_id') (fin del paso 2) `${BASE_URL}/booking/${bookingId}` (URL del paso 3). El id que la reserva generó se extrae y se mete en la URL de la confirmación. Este es el enlace que no podrías hardcodear: cada reserva produce un booking_id distinto.
  • El check 'price matches quote' del paso 3 cierra la cadena: verifica que el precio que la confirmación devuelve es el mismo que se cotizó al principio. Si algún eslabón se rompiera, este check lo atraparía.

Fíjate en que las variables (price, bookingId) se declaran fuera de los group() para que el valor sobreviva de un paso al siguiente. Ese "guardar entre pasos" es el mecanismo físico de la correlación: un valor que sale de una respuesta y espera hasta la próxima petición.

El flujo completo, ejecutado en Python

En Python, extraer es leer una clave del body (body.get("price_cents"), b2.get("booking_id")); reusar es meter ese valor en el siguiente request. El generador corre el flujo de tres pasos con las dos correlaciones reales y checks en cada paso:

# El nucleo del escenario correlacionado (una iteracion de un VU).
# --- Paso 1: cotizar --> extraer price ---
s1, b1 = post_json(f"{BASE_URL}/quote", {"room": room, "tier": tier, "hours": hours})
price = b1.get("price_cents")                       # EXTRAE el precio

# --- Paso 2: reservar (usa price) --> extraer booking_id ---
# CORRELACION 1: el price viaja en el body de /book
s2, b2 = post_json(f"{BASE_URL}/book",
                   {"room": room, "tier": tier, "hours": hours, "price_cents": price})
booking_id = b2.get("booking_id")                   # EXTRAE el id

# --- Paso 3: confirmar (usa booking_id) ---
# CORRELACION 2: el booking_id viaja en la URL de GET /booking/<id>
s3, b3 = get_json(f"{BASE_URL}/booking/{booking_id}")

# checks del paso 3 que cierran la cadena:
record("confirm: id matches", b3.get("booking_id") == booking_id)
record("confirm: price matches quote", b3.get("price_cents") == price)

El dato parametrizado (lección 5) elige room, tier, hours al inicio; a partir de ahí, price y booking_id fluyen de una respuesta a la siguiente. Corramos el escenario sano, 4 VUs / 3 s / think 100 ms, con checks en cada paso.

Qué esperar. Si la correlación funciona, los tres pasos deben pasar sus checks al 100 %: el precio cuadra, la reserva se confirma con un booking_id real, y la confirmación encuentra ese id con ese precio. Salida real en este entorno:

----------------------------------------------------------------
  Escenario cotizar->reservar->confirmar  ->  4 VUs / 3s / think 100ms
----------------------------------------------------------------
  vus............: 4
  iterations.....: 109   (35.2/s)
  checks.........: 100.00%   (872 de 872)
    quote: status is 200..............:  109 ok / 0    fail
    quote: price is correct...........:  109 ok / 0    fail
    book: status is 200...............:  109 ok / 0    fail
    book: confirmed is true...........:  109 ok / 0    fail
    book: booking_id present..........:  109 ok / 0    fail
    confirm: status is 200............:  109 ok / 0    fail
    confirm: id matches...............:  109 ok / 0    fail
    confirm: price matches quote......:  109 ok / 0    fail
  http_errors....: 0
  por grupo (latencia media, n llamadas):
    group 'quote  '.....: n=109  avg=1.06ms
    group 'book   '.....: n=109  avg=0.54ms
    group 'confirm'.....: n=109  avg=0.47ms

Léela como la cadena de custodia funcionando:

  • 8 checks por iteración, los 8 en verde (872 = 109 × 8). Dos del paso de cotizar, tres del de reservar, tres del de confirmar. La cadena completa se cumplió en las 109 iteraciones.
  • confirm: id matches: 109 ok / 0 fail — este es el check que prueba la correlación del id. La confirmación pidió GET /booking/<booking_id> con el id que la reserva devolvió, y la API encontró exactamente esa reserva y devolvió ese mismo id. Si el escenario hubiera inventado el id, este check habría fallado 109 veces (404, id no encontrado). El 109 ok es la prueba de que el booking_id extraído es el real.
  • confirm: price matches quote: 109 ok / 0 fail — este cierra la cadena entera. El precio que la confirmación devuelve es el mismo que se cotizó tres pasos atrás. El price_cents viajó de la cotización a la reserva y quedó guardado en la reserva; la confirmación lo lee y coincide. Los tres eslabones de la custodia, intactos.
  • El desglose por grupo confirma que cada paso corrió 109 veces (uno por iteración), con la cotización como la más pesada (lección 4).

Declaración: para la correlación, este servidor genera el booking_id con un contador secuencialbk-000001, bk-000002, …—, un id único por reserva. Es distinto del formato descriptivo bk_Focus_basic_3 que usa el servidor canónico de M1 (que repetiría el mismo id entre reservas con los mismos datos); aquí necesitamos ids únicos para que la correlación tenga sentido. Y esa unicidad es justo lo que la hace interesante: cada booking_id fue distinto (bk-000001, bk-000002, …, generados al vuelo por la API), y aun así el paso 3 siempre encontró el suyo, porque lo extrajo de la respuesta en vez de inventarlo. Eso es la correlación: seguir el hilo que el servidor genera, no adivinarlo.

Qué pasaría si rompieras la cadena

Para que quede claro por qué la correlación importa, imagina que en el paso 3 usaras un id inventado en vez del extraído —por ejemplo, un bk-999999 fijo—. La cadena se rompería:

  • GET /booking/bk-999999 devolvería 404 (esa reserva no existe; nadie la creó).
  • El check confirm: status is 200 fallaría en todas las iteraciones (404, no 200).
  • El check confirm: id matches fallaría también (no hay id que coincidir).

Y peor: la prueba parecería "correr" (completaría las iteraciones), pero estaría probando una confirmación que nunca funciona, dándote datos de latencia de un 404 en vez de una confirmación real. Ese es el peligro de no correlacionar: no es solo que falle, es que puede fallar silenciosamente en el sentido correcto (la petición se hace, pero contra un recurso que no existe), y si no miras los checks, ni te enteras. La correlación —extraer el id real— es lo que hace que el paso 3 pruebe lo que dices que prueba.

Errores comunes

Hardcodear el valor del enlace en vez de extraerlo. Qué pasa: alguien pone un booking_id fijo (bk-000001) en el paso de confirmar en vez de usar el que devolvió la reserva. Por qué pasa: parece más simple, y quizás en la primera iteración hasta funciona (si ese id existe por casualidad). Cómo detectarlo: el paso de confirmar falla en casi todas las iteraciones (404), o "pasa" contra un recurso que no es el que creó esta iteración. Cómo corregirlo: extrae el id de la respuesta de la reserva (res.json('booking_id')) y úsalo. Un valor generado al vuelo por el servidor nunca se hardcodea.

No guardar el valor extraído fuera del grupo/paso. Qué pasa: en k6, alguien declara const price = res.json(...) dentro del group('quote', ...), y en el paso de reservar price ya no existe (quedó en el ámbito del grupo). Por qué pasa: se olvida que el valor tiene que sobrevivir de un paso al siguiente. Cómo detectarlo: undefined en el body de la reserva, o un ReferenceError. Cómo corregirlo: declara la variable fuera de los group() (con let price; antes del primer grupo) y asígnala dentro. El valor correlacionado necesita vivir entre pasos.

No verificar la correlación con un check. Qué pasa: alguien correlaciona el id pero no pone un check que confirme que el paso 3 lo encontró, así que si la extracción falla (id undefined), la prueba no lo nota. Por qué pasa: se confía en que "si llegó hasta aquí, funcionó". Cómo detectarlo: la prueba corre pero nunca sabrías si el paso 3 pega contra reservas reales o contra 404. Cómo corregirlo: pon un check en el paso correlacionado —'id matches', 'status is 200'— que falle si el id no era el real. El check es lo que convierte "la petición se hizo" en "la petición hizo lo correcto".

Ejercicios

Ejercicio 1 — Identifica las dos correlaciones. En el flujo cotizar → reservar → confirmar, hay dos valores que se extraen de una respuesta y se reusan en la siguiente. (a) Nómbralos. (b) Para cada uno, di de qué respuesta se extrae y en qué petición se reusa (y si va en el body o en la URL).

Ver solución
  • (a) El price_cents y el booking_id.
  • (b)
    • price_cents: se extrae de la respuesta de POST /quote y se reusa en el body de POST /book.
    • booking_id: se extrae de la respuesta de POST /book y se reusa en la URL de GET /booking/<id>.

Ejercicio 2 — El check que prueba la correlación. En la corrida real, confirm: id matches dio 109 ok / 0 fail. (a) ¿Qué prueba exactamente ese resultado sobre la correlación? (b) Si el escenario hubiera usado un booking_id inventado (fijo), ¿qué habría mostrado ese check y por qué?

Ver solución
  • (a) Prueba que el booking_id que el paso 3 usó en la URL era el real —el que la reserva generó y devolvió—, porque la API encontró exactamente esa reserva y devolvió el mismo id. Las 109 coincidencias confirman que la extracción y el reuso funcionaron en cada iteración, con un id distinto cada vez.
  • (b) Habría mostrado 0 ok / 109 fail (o similar). Un id inventado como bk-999999 no corresponde a ninguna reserva creada, así que GET /booking/bk-999999 devolvería 404, y tanto status is 200 como id matches fallarían en todas las iteraciones. El check habría delatado que la cadena estaba rota.

Ejercicio 3 — ¿Por qué no se puede hardcodear el booking_id? Explica, con la analogía de la cadena de custodia, por qué el price_cents podría en teoría precalcularse pero el booking_id no puede hardcodearse nunca. ¿Qué diferencia a los dos valores?

Ver solución

La diferencia es cuándo y quién genera cada valor.

El price_cents es determinista: para Studio/pro/4h, siempre es 12800, calculado con una regla conocida. En teoría podrías precalcularlo (de hecho, el dataset de la lección 5 lo trae como expected). Correlacionarlo desde la respuesta es más honesto (pruebas el precio que la API realmente dio), pero el valor es predecible.

El booking_id lo genera el servidor en tiempo de ejecución, y es distinto en cada reserva (bk-000001, bk-000002, …). No existía antes de que el paso de reservar lo creara. En la cadena de custodia, es el número de seguimiento que el guardia escribe en el momento que llega el paquete: el mensajero no puede adivinarlo, solo leerlo. Por eso el booking_id solo se puede obtener extrayéndolo de la respuesta de la reserva; hardcodearlo garantiza que apuntes a la reserva equivocada (o a ninguna). Un valor que nace al vuelo nunca se puede fijar de antemano.

Resumen y siguiente paso

En esta lección construiste el corazón del escenario realista: la correlación, extraer un valor de una respuesta y reusarlo en la petición siguiente. El flujo cotizar → reservar → confirmar es una cadena de custodia: el price_cents que te cotizaron viaja a la reserva; el booking_id que la reserva generó viaja a la URL de la confirmación. Lo corriste de verdad en Python: 4 VUs / 3 s dieron 872 de 872 checks (100 %), y en particular confirm: id matches y confirm: price matches quote en 109 ok / 0 fail probaron que el booking_id extraído era el real y que el precio viajó intacto por los tres pasos —con un booking_id distinto en cada iteración, siempre encontrado porque se extrajo en vez de inventarse—. Y quedó claro qué pasa si rompes la cadena (un id inventado da 404 en cascada): la correlación es lo que hace que el paso siguiente pruebe lo que dices que prueba.

Antes de avanzar deberías poder: identificar los valores correlacionados de un flujo (qué se extrae y dónde se reusa); escribir la extracción (res.json('clave')) y el reuso (en body o URL); guardar el valor entre pasos (fuera de los grupos); verificar la correlación con un check; y explicar por qué un id generado al vuelo nunca se hardcodea.

La lección 7 pone el último toque de realismo: el think time con jitter. Un escenario correlacionado como el tuyo, corriendo sin pausa, dispara los tres pasos a máxima velocidad —un martillo, no un usuario—. Y si todos los VUs pausan exactamente lo mismo, crean picos artificiales (todos piden a la vez). Aprenderás a poner una pausa aleatoria entre pasos para desincronizar los VUs, y verás medido el efecto: el mismo escenario a 2158 iteraciones/s sin pausa contra 13/s con think time realista.

Recursos