Módulo 6: Checks, groups y escenarios realistas

2. `check()` para la corrección bajo carga

Descripción

En el módulo 2 conociste check() en su forma básica: verificar que una respuesta cumple ciertos criterios con nombre y contar cuántos pasan. Ahora lo pones a hacer el trabajo para el que de verdad sirve en una prueba de carga realista: verificar la corrección. La distinción es más profunda de lo que parece. Un sistema puede estar respondiendo —status 200, latencia baja— y sin embargo estar equivocándose: devolver un precio mal calculado, un body al que le falta una clave, un tipo de dato cambiado. Bajo carga esto pasa más de lo que uno cree, porque el estrés destapa condiciones de carrera, cachés corruptas, valores por defecto que se cuelan. Una prueba que solo mira el status te dará un verde tranquilizador mientras la API entrega basura rápida.

Verificar la corrección significa comprobar tres cosas sobre cada respuesta, no una. La primera es el status: ¿la API respondió sin error? La segunda es el valor: ¿el price_cents es exactamente el que debía ser, calculado con la misma regla de negocio? La tercera es la forma (o shape) del body: ¿la respuesta trae las claves que prometió, con los tipos correctos —price_cents presente y siendo un entero, no un null, no un string—? Las tres juntas responden la pregunta que de verdad importa: no "¿respondió?", sino "¿respondió bien?". En esta lección escribes esos tres checks y los ejecutas de verdad contra la API canónica, con datos variados, para ver la tasa de checks salir al 100 % cuando la API está sana.

Conexión con el módulo: esta lección es la primera de las cuatro piezas del escenario realista. El check() de k6 se muestra como contenido; los tres criterios —status, valor, forma— los ejecutamos de verdad en el generador Python contra la API canónica, contando checks reales sobre cotizaciones con datos variados. Aquí verificamos un solo paso (/quote); encadenar varios pasos con checks en cada uno es la correlación de la lección 6. Todo lo rotulado como salida de Python fue medido en este entorno con Python 3.14.0. La regla de siempre: el precio se compara como entero en centavos, con la misma fórmula que la API (tarifa × horas, descuento pro entero).

El inspector que revisa tres cosas, no una

Vuelve al inspector de calidad de la fábrica que conociste en el módulo 2, pero fíjate ahora en qué revisa. Un inspector malo mira solo si la pieza salió de la máquina: "¿hay una pieza aquí? Sí. Aprobada." Con ese criterio, una pieza deforme, del material equivocado o a la que le falta un agujero pasaría igual, porque existe. Un inspector bueno revisa una lista: ¿tiene la medida correcta? ¿el material correcto? ¿los agujeros en su sitio? Solo si las tres cosas cuadran, la pieza es buena. La diferencia entre los dos inspectores no es cuántas piezas revisan, sino cuántos criterios aplican a cada una.

En una prueba de carga, verificar solo el status es ser el inspector malo: "¿respondió la API? Sí (200). Aprobada." Pero una respuesta 200 con el precio equivocado es una pieza deforme que pasó el control. El inspector bueno aplica tres criterios a cada respuesta: status (¿salió sin error?), valor (¿el precio es exactamente el correcto?) y forma (¿el body tiene las claves y tipos que prometió?). Bajo carga, cuando el sistema empieza a fallar de formas raras, el criterio de valor y el de forma son los que atrapan lo que el status deja pasar. Un check() con varios criterios es ese inspector completo.

Verificar la corrección bajo carga es aplicar tres criterios a cada respuesta, no uno: status (¿respondió sin error?), valor (¿el price_cents es exactamente el correcto?) y forma (¿el body trae las claves y tipos esperados?). Verificar solo el status es aprobar piezas deformes por el hecho de que existen. El check de valor y el de forma atrapan lo que el status deja pasar.

Los tres checks en k6 (contenido)

Retomamos la cotización, ahora con los tres criterios de corrección. Cada clave del objeto es el nombre del criterio; cada valor, una función que recibe la respuesta (r) y devuelve true o false:

// quote_correctness.js - verificar la CORRECCION de una cotizacion.
// MOSTRADO COMO CONTENIDO: k6 no esta instalado en este entorno.
import http from 'k6/http';
import { check } from 'k6';

const BASE_URL = 'http://localhost:8000';

export default function () {
  const payload = JSON.stringify({ room: 'Focus', tier: 'basic', hours: 3 });
  const params = { headers: { 'Content-Type': 'application/json' } };
  const res = http.post(`${BASE_URL}/quote`, payload, params);

  check(res, {
    // (1) status: la API respondio sin error
    'status is 200': (r) => r.status === 200,
    // (2) valor: el precio es EXACTAMENTE el correcto (7500 para Focus/basic/3h)
    'price is correct': (r) => r.json('price_cents') === 7500,
    // (3) forma: el body trae price_cents y es un entero (no null, no string)
    'body shape is valid': (r) =>
      typeof r.json('price_cents') === 'number' &&
      Number.isInteger(r.json('price_cents')),
  });
}

Desármalo criterio por criterio:

  • 'status is 200'. El criterio más básico: ¿la API respondió sin error? Necesario, pero lejos de suficiente. Un 200 dice "la API funcionó", no "la API funcionó bien".
  • 'price is correct'. El criterio de valor. Lee price_cents del body (con r.json) y comprueba que sea exactamente 7500 —el número ancla, comparado como entero, porque el dinero va en centavos int—. Este check atrapa el bug que el status no ve: una API que responde 200 pero con el precio mal calculado.
  • 'body shape is valid'. El criterio de forma. No mira qué precio es, sino que el body tenga la estructura prometida: que price_cents sea un número y además un entero. Atrapa una clase distinta de bug —una API que devuelve {"price_cents": null}, o {"price_cents": "7500"} (string), o que se olvidó la clave— que ni el status ni una comparación de valor mal escrita detectarían siempre.

Los tres juntos cubren tres formas distintas de fallar: no responder (status), responder con el número equivocado (valor) y responder con una estructura rota (forma). Cada uno atrapa lo que los otros dejan pasar.

Los mismos tres checks, ejecutados en Python

En el generador de Python, cada VU hace las mismas tres comprobaciones tras cada cotización. Y para que la prueba sea realista desde ya, los datos se varían: cada iteración elige una fila distinta (varias salas, tiers y horas), no siempre Focus/basic/3h. El precio esperado se calcula con la misma regla que la API:

# Los tres criterios de correccion, ejecutados por cada VU.
ROOM_RATES = {"Focus": 2500, "Studio": 4000, "Boardroom": 8000}

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

# ... tras recibir status y body de POST /quote:
ok_status = status == 200                                              # (1) status
ok_price  = body.get("price_cents") == expected_price(room, tier, hours)  # (2) valor
ok_shape  = "price_cents" in body and isinstance(body["price_cents"], int)  # (3) forma
# cada True suma a su contador ok; cada False, a su contador fail

Fíjate en el criterio de forma en Python: "price_cents" in body and isinstance(body["price_cents"], int) comprueba que la clave exista y que su valor sea un entero. Es el equivalente exacto del typeof ... === 'number' && Number.isInteger(...) de k6. Los tres criterios son los mismos en las dos caras.

Corramos el generador con 4 VUs durante 3 segundos (con un think time chico de 50 ms, para números legibles). Cada iteración hace tres checks, así que esperamos 3 × iteraciones checks en total, todos pasando si la API está sana.

Qué esperar. Con la API correcta, los tres criterios deben pasar siempre para cualquier dato: 100 % de checks. Salida real en este entorno:

  vus............: 4
  iterations.....: 209
  checks.........: 100.00%   (627 de 627)
    status is 200...............:  209 ok / 0    fail
    price is correct............:  209 ok / 0    fail
    body has price_cents:int....:  209 ok / 0    fail

Léela:

  • iterations: 209 — 4 VUs golpeando localhost con un think time de 50 ms completaron 209 cotizaciones en 3 segundos. Cada una con un dato posiblemente distinto (Focus, Studio o Boardroom; basic o pro; horas variadas).
  • checks: 100.00% (627 de 627) — cada iteración hace 3 checks (status, valor, forma), así que 209 iteraciones dan 627 checks. Los 627 pasaron: para cada uno de los datos variados, la API respondió 200, con el precio exactamente correcto y con la forma esperada.
  • El desglose por criteriostatus is 200: 209 ok / 0 fail, price is correct: 209 ok / 0 fail, body has price_cents:int: 209 ok / 0 fail— es justo lo que el resumen de k6 te daría con los nombres que pusiste. Los tres en verde: la API está correcta, no solo viva.

Que el precio cuadrara para todas las salas y tiers —no solo para Focus/basic— es la prueba de que el check de valor está bien hecho: usa la misma fórmula que la API (tarifa × horas, descuento pro entero) para cualquier dato, no un número mágico fijo. Eso es lo que lo hace útil cuando parametrizas (lección 5).

Por qué el status solo no basta (el bug que se esconde detrás de un 200)

Imagina que la API, bajo estrés, empieza a devolver el precio sin aplicar el descuento pro —un bug clásico: la rama del descuento se salta por una condición de carrera—. La respuesta seguiría siendo un 200 perfecto, con un body bien formado ({"price_cents": 7500} para Focus/pro/3h, cuando debería ser 6000). ¿Qué check lo atraparía?

  • El status (status is 200): verde. La API respondió sin error. No nota nada.
  • La forma (body has price_cents:int): verde. El body trae price_cents y es un entero. Tampoco nota nada —7500 es un entero perfectamente válido, solo que es el número equivocado—.
  • El valor (price is correct): rojo. 7500 != expected_price("Focus", "pro", 3), que es 6000. Solo este criterio atrapa el bug.

Esta es la moraleja de la lección, y la verás medida en la lección 3: hay bugs que solo el check de valor detecta. Un sistema bajo carga puede responder rápido, con status 200 y forma válida, y aun así estar calculando mal. Si tu prueba solo mira el status —o incluso status + forma—, ese bug pasa invisible. El check de valor, que compara contra lo que la respuesta debía ser, es el que protege la corrección de verdad.

(Y ojo: los tres criterios se complementan, no se sustituyen. El de forma atrapa null/string/clave-faltante que el de valor a veces ni podría comparar; el de valor atrapa el número equivocado que el de forma aprueba. Por eso van los tres.)

Errores comunes

Verificar solo el status y llamarlo "corrección". Qué pasa: alguien pone únicamente 'status is 200' y cree que su prueba verifica que la API funciona bien. Por qué pasa: un 200 se siente como "todo correcto". Cómo detectarlo: si la API devolviera un precio equivocado con status 200, tu prueba quedaría 100 % verde y no lo notarías. Cómo corregirlo: añade el check de valor (price_cents correcto) y el de forma (la clave existe y es del tipo esperado). El status es el primero de tres criterios, no el único.

Comparar el precio contra un número mágico fijo. Qué pasa: alguien escribe r.json('price_cents') === 7500 y lo deja así incluso cuando el dato varía. Por qué pasa: funciona mientras solo cotices Focus/basic/3h. Cómo detectarlo: en cuanto parametrizas (cotizas Studio, o pro, o más horas), el check falla para todo lo que no sea Focus/basic/3h, porque 7500 ya no es el precio correcto. Cómo corregirlo: calcula el esperado con la misma regla que la API (expected_price(room, tier, hours)) para el dato de esa iteración. Un número fijo solo sirve si el dato es fijo.

Olvidar el check de forma y romperse con un null. Qué pasa: alguien verifica solo status y valor, y cuando la API devuelve {"price_cents": null} bajo estrés, la comparación de valor (null == 6000) da False —lo cual está bien—, pero en otros lenguajes o con otros accesos el null podría reventar el código del check en vez de contarlo como fallo. Por qué pasa: se asume que el body siempre trae la clave con el tipo correcto. Cómo detectarlo: errores raros o checks que no se cuentan cuando el body viene malformado. Cómo corregirlo: añade el check de forma que comprueba explícitamente que la clave existe y es del tipo esperado, antes de razonar sobre su valor.

Ejercicios

Ejercicio 1 — Escribe los tres checks para /book. Escribe (en k6, como contenido) un check para una respuesta de POST /book que verifique la corrección con tres criterios: status 200, que confirmed sea exactamente true (valor booleano), y que el body tenga un booking_id que sea un string no vacío (forma). Pon nombres descriptivos.

Ver solución
check(res, {
  'status is 200': (r) => r.status === 200,
  'booking is confirmed': (r) => r.json('confirmed') === true,
  'booking_id shape is valid': (r) =>
    typeof r.json('booking_id') === 'string' && r.json('booking_id').length > 0,
});
  • status is 200 es el criterio de status.
  • confirmed === true es el criterio de valor: no solo que la clave exista, sino que valga exactamente true (no "true", no 1).
  • El tercer criterio es la forma: booking_id presente, de tipo string y no vacío. (Podrías afinarlo comprobando que empieza con 'bk-'.)

Ejercicio 2 — ¿Qué criterio atrapa cada bug? Para cada fallo que la API de Reservo podría tener bajo carga, di cuál de los tres criterios (status, valor, forma) lo atraparía. (a) La API devuelve 500 en vez del precio. (b) La API devuelve 200 con {"price_cents": 7500} para una cotización Focus/pro/3h (debería ser 6000). (c) La API devuelve 200 con {"price_cents": null}. (d) La API devuelve 200 con {"cost": 6000} (la clave se llama mal).

Ver solución
  • (a) El criterio de status (status is 200 falla, porque es 500).
  • (b) El criterio de valor (price is correct falla: 7500 != 6000). El status es 200 y la forma es válida; solo el valor lo atrapa.
  • (c) El criterio de forma (price_cents no es un entero, es null). El de valor también daría False (null != ...), pero el de forma es el que lo diagnostica limpiamente.
  • (d) El criterio de forma (price_cents no está en el body: la clave es cost). El de valor no podría ni leer price_cents.

Los tres criterios cubren tres familias de fallo distintas; por eso van juntos.

Ejercicio 3 — Lee el conteo. En la corrida real, el resumen dijo checks: 100.00% (627 de 627) con 209 iteraciones. (a) ¿Por qué 627 y no 209? (b) Si el criterio de valor hubiera fallado en 40 de las 209 cotizaciones (y los otros dos siguieran verdes), ¿qué porcentaje total de checks mostraría el resumen? (c) ¿Qué habrías dejado de ver si solo hubieras verificado el status?

Ver solución
  • (a) Porque hay 3 checks por iteración (status, valor, forma): 209 × 3 = 627 checks totales. El "de 627" del resumen es el total de comprobaciones, no de iteraciones.
  • (b) El status y la forma seguirían verdes (209 + 209 = 418 ok), y el valor tendría 169 ok / 40 fail. Total: 418 + 169 = 587 ok de 627 = 93.62 %. El fallo en un solo criterio baja el total, y el desglose te diría exactamente cuál (el de valor).
  • (c) Habrías dejado de ver cualquier bug de valor o de forma: si la API respondiera 200 con el precio equivocado o el body malformado, el check de status quedaría 100 % verde y darías la API por sana. Solo los checks de valor y forma revelan que respondió mal.

Resumen y siguiente paso

En esta lección check() pasó de "¿respondió la API?" a "¿respondió bien?". Verificar la corrección bajo carga es aplicar tres criterios a cada respuesta: status (¿sin error?), valor (¿el price_cents es exactamente el correcto, calculado con la regla de negocio?) y forma (¿el body trae las claves y tipos prometidos?). Lo ejecutaste de verdad en Python con datos variados: 4 VUs / 3 s dieron 627 de 627 checks (100 %), con el precio cuadrando para todas las salas y tiers porque el esperado se calcula con la misma fórmula que la API. Y quedó clara la lección de fondo: hay bugs —un precio mal calculado, un body malformado— que un status 200 esconde, y que solo el check de valor o el de forma atrapan.

Antes de avanzar deberías poder: escribir un check con los tres criterios de corrección y nombres descriptivos; calcular el precio esperado con la misma regla que la API para verificar el valor con datos variados; explicar por qué verificar solo el status es insuficiente; y decir qué familia de bug atrapa cada criterio.

La lección 3 vuelve sobre una promesa del módulo 2: un check que falla no detiene la corrida. Ahora lo ves con consecuencias —el check mide pero no decide—, y lo contrastas con el threshold (módulo 5), que sí da el veredicto pasa/falla con exit code. Correrás la misma cotización con un check de precio con bug (la iteración completa igual, pero la tasa de checks baja) y le pondrás encima un gate de threshold que la haga fallar de verdad.

Recursos

  • Checks en k6 — la referencia de check(): la sintaxis con criterios con nombre y por qué un check fallido no aborta la prueba. La fuente exacta de esta lección.
  • El objeto Response — k6/httpr.status y r.json(), lo que los tres criterios del check inspeccionan. Cómo se lee el status, el valor y la forma de la respuesta.
  • isinstance — funciones incorporadas de Python — la función con la que el check de forma comprueba que price_cents es un entero. Cómo se verifica el tipo de un valor en Python.
  • Google SRE Book — Service Level Objectives — el marco de por qué "correcto" es una dimensión de calidad tan medible como "rápido". El fondo conceptual de verificar la corrección, no solo la latencia.