Módulo 2: El script de k6 y los usuarios virtuales
4. `check()`: verificar la respuesta
Descripción
Una prueba de carga que solo mide velocidad puede mentirte. Imagina que la API de Reservo, bajo la presión de cientos de usuarios, empieza a responder rapidísimo... porque devuelve un error 500 vacío en vez de calcular el precio. Las latencias se verían excelentes —¡nunca fue tan rápida!—, y sin embargo la aplicación está rota. La lección es incómoda: rápido no es lo mismo que correcto. Por eso, junto a las métricas de tiempo, k6 te da una herramienta para verificar que cada respuesta fue válida: la función check().
Un check es una comprobación con nombre: le das la respuesta y un conjunto de condiciones —"el status es 200", "el precio es 7500"— y k6 evalúa cada una y cuenta cuántas pasaron y cuántas fallaron. Se parece a un assert de los tests unitarios, pero con una diferencia crucial que define su papel en una prueba de carga: un check que falla no aborta la prueba. La corrida sigue, los demás VUs siguen golpeando, y al final el resumen te dice "el 98 % de los checks pasó". Esa tolerancia es deliberada: bajo carga quieres medir cuántas respuestas salieron mal, no detenerte en la primera. En esta lección aprendes a escribir checks, a leer su conteo, y a verlos atrapar un bug de verdad.
Conexión con el módulo: la lección 3 te enseñó a leer la respuesta (res.status, res.json('price_cents')); esta convierte esa lectura en una verificación formal. El check() de k6 se muestra como contenido; el mismo criterio —status 200 y precio correcto— lo ejecutamos de verdad en el generador Python contra la API canónica, contando checks reales. Y para que veas que un check sirve, correremos una versión con un bug en el precio esperado y lo veremos fallar de forma medible. Todo lo rotulado como salida de Python fue medido en este entorno con Python 3.14.0. Aquí check() se ve en su forma básica; su uso a fondo —agrupar con group(), parametrizar datos, correlacionar cotizar→reservar— es el módulo 6.
El inspector de calidad al final de la línea
Piénsalo así. En una fábrica que produce miles de piezas por hora, al final de la banda hay un inspector de calidad. No detiene la línea cada vez que ve una pieza defectuosa —eso pararía la producción entera y no sabrías cuántas fallan—. En vez de eso, revisa cada pieza contra una lista de criterios ("¿tiene la medida correcta?", "¿el color es el esperado?"), marca las buenas y las malas, y al final del turno reporta: "de 10 000 piezas, 9 800 pasaron, 200 fallaron el criterio de medida". Ese reporte es oro: te dice qué falla y cuánto, sin haber parado la fábrica.
check() es ese inspector. Por cada respuesta que vuelve, evalúa una lista de criterios con nombre —"status is 200", "price is 7500"— y lleva la cuenta de cuántas veces pasó cada uno. No aborta la corrida cuando algo falla (eso sería parar la línea); sigue midiendo, y al final el resumen te da el reporte: "checks 98.04 %, 800 de 816". Comparado con un assert de un test unitario —que sí detiene todo al primer fallo, como un inspector que apaga la fábrica—, el check está hecho para el volumen: quieres el porcentaje de respuestas malas bajo carga, no un frenazo.
check()verifica que una respuesta cumple ciertos criterios con nombre y cuenta cuántas pasan y cuántas fallan, pero NO aborta la prueba cuando algo falla —a diferencia de unassert—. Es un inspector de calidad que reporta el porcentaje de piezas malas sin parar la línea. Bajo carga quieres medir cuántas respuestas salen mal, no detenerte en la primera.
check() en k6 (contenido)
Retomamos el guion de cotizar, ahora con la verificación. El check recibe la respuesta y un objeto donde cada clave es el nombre del criterio y cada valor es una función que recibe la respuesta (r) y devuelve true o false:
// quote_check.js — cotizar y verificar la respuesta.
// 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(respuesta, { 'nombre del criterio': (r) => condicion booleana })
check(res, {
'status is 200': (r) => r.status === 200,
'price is 7500': (r) => r.json('price_cents') === 7500,
});
}
Desármalo:
check(res, { ... }). El primer argumento es la respuesta a inspeccionar; el segundo, un objeto con uno o más criterios. Aquí hay dos.'status is 200': (r) => r.status === 200. La clave'status is 200'es el nombre que verás en el resumen —elígelo descriptivo, porque es lo que leerás cuando algo falle—. El valor es una función que recibe la respuestary devuelvetruesi el status es 200. Este es el criterio más básico: ¿la API respondió bien?'price is 7500': (r) => r.json('price_cents') === 7500. El segundo criterio va más allá del status: comprueba el contenido. Leeprice_centsdel cuerpo (conres.json, como en la lección 3) y verifica que sea exactamente7500—el número ancla, comparado como entero, porque el dinero va en centavosint—. Este check atrapa un bug que el status no vería: una API que responde 200 pero con el precio equivocado.
Fíjate en la pareja: verificar status 200 dice "la API funcionó"; verificar el precio dice "la API funcionó bien". Bajo carga, ambos importan: un sistema estresado puede seguir respondiendo 200 pero empezar a calcular mal, y solo el segundo check lo detecta.
El mismo check, ejecutado en Python
En el generador de Python, cada VU hace las mismas dos comprobaciones tras cada cotización: compara el status con 200 y el price_cents recibido contra el precio esperado (que calculamos con la misma regla que la API: tarifa × horas, con descuento pro entero). Cada comprobación que pasa suma a checks_passed; cada una que falla, a checks_failed:
# El equivalente del check() de k6: dos comprobaciones por respuesta.
def expected_price(room, tier, hours):
rate = {"Focus": 2500, "Studio": 4000, "Boardroom": 8000}[room]
total = rate * hours
if tier == "pro":
total = total * 80 // 100 # descuento pro entero (centavos int)
return total
# ... dentro del guion de cada VU, tras recibir la respuesta:
ok_status = status == 200
ok_price = body.get("price_cents") == expected_price(room, tier, hours)
# se cuentan: cada True suma a checks_passed, cada False a checks_failed
Corramos el generador con 3 VUs durante 3 segundos (con think time de un segundo, para números chicos y legibles). Cada iteración hace dos checks, así que esperamos 2 × iteraciones checks en total, todos pasando si la API está sana.
Qué esperar. Con la API correcta, los dos criterios deben pasar siempre: 100 % de checks. Salida real en este entorno:
----------------------------------------------------------
Generador de carga Python -> 3 VUs / 3s / think 1000ms
----------------------------------------------------------
vus............: 3
duracion real..: 3.04s
iterations.....: 9 (3.0/s)
checks.........: 100.00% (18 de 18)
status is 200....: 9 ok / 0 fail
price is correct.: 9 ok / 0 fail
http_errors....: 0
req_duration...: avg=3.20ms min=0.86ms max=6.95ms
----------------------------------------------------------
Léela:
iterations: 9— 3 VUs × ~3 vueltas cada uno (una por segundo por elsleep(1)) = 9 iteraciones.checks: 100.00% (18 de 18)— cada iteración hace 2 checks (status y precio), así que 9 iteraciones dan 18 checks. Los 18 pasaron: la API respondió 200 y el precio siempre cuadró.status is 200: 9 ok / 0 failyprice is correct: 9 ok / 0 fail— el desglose por criterio. Este desglose es justo lo que el resumen de k6 te da con los nombres que pusiste en elcheck. La API está sana: verde en ambos.
Un 100 % de checks es tranquilizador, pero también es sospechoso —¿de verdad el check hace algo, o siempre pasa?—. Para probar que un check sirve, hay que verlo fallar.
Ver el check atrapar un bug
Provoquemos un fallo con sentido. Imagina que el código que verifica el precio tiene un bug: olvida el descuento pro. Es decir, para una cotización pro espera el precio basic (sin el 20 % de descuento), que no coincide con lo que la API —correctamente— devuelve. Un check bien escrito debería atrapar justo esa discrepancia:
# VARIANTE CON BUG: el precio esperado olvida el descuento pro.
def buggy_expected(room, tier, hours):
rate = {"Focus": 2500, "Studio": 4000, "Boardroom": 8000}[room]
return rate * hours # BUG: ignora el tier 'pro' (no aplica *80//100)
Con este esperado defectuoso, las cotizaciones basic seguirán cuadrando (no llevan descuento), pero las pro fallarán el check de precio: la API devuelve el precio con descuento y el esperado (buggy) trae el precio sin descuento. Como el generador elige tier al azar entre basic y pro, aproximadamente la mitad de los checks de precio deberían fallar. Corramos 3 VUs / 3 s (con think time de 200 ms):
Qué esperar. El check de status seguirá al 100 % (la API responde 200 siempre), pero el check de precio fallará en las cotizaciones pro. Salida real en este entorno:
----------------------------------------------------------
Generador con BUG en el check de precio -> 3 VUs / 3s / think 200ms
----------------------------------------------------------
iterations.....: 45
checks.........: 76.67% (69 de 90)
status is 200....: 45 ok / 0 fail
price is correct.: 24 ok / 21 fail <-- el check atrapa los precios pro
----------------------------------------------------------
Esta salida enseña de qué sirve un check:
checks: 76.67% (69 de 90)— de 90 checks totales (45 iteraciones × 2 criterios), 69 pasaron y 21 fallaron. El porcentaje bajó de 100 % a 76.67 %: el resumen te avisa que algo no cuadra, con un número.status is 200: 45 ok / 0 fail— el status siguió perfecto. La API nunca dejó de responder 200. Si solo hubieras verificado el status, no habrías notado nada: verde total, y sin embargo hay un problema.price is correct: 24 ok / 21 fail— aquí está el bug, atrapado. De 45 cotizaciones, ~24 eranbasic(cuadran) y ~21 eranpro(fallan, porque el esperado olvidó el descuento). El check de contenido —no el de status— fue el que detectó la discrepancia. Esa es la moraleja: verificar solo el status es insuficiente; verificar el valor de la respuesta es lo que atrapa los bugs de lógica bajo carga.
(En este ejemplo el bug está en el esperado del test, para ilustrar; en la vida real el bug estaría en la API y el check te avisaría igual. En ambos casos, el check hizo su trabajo: señaló que la respuesta no era la que debía ser, y lo hizo con un número, sin abortar la corrida.)
check no es assert: por qué la corrida no se detuvo
Un detalle que ya viste en acción y conviene subrayar: en la corrida con bug, a pesar de 21 checks fallidos, la prueba completó sus 45 iteraciones. No se detuvo en el primer fallo. Esa es la diferencia esencial con un assert de un test unitario:
- Un
assert(pytest, unittest) aborta en cuanto una condición no se cumple: el test se marca como fallido y se detiene. Perfecto para tests de corrección, donde quieres saber si algo está mal. - Un
check(k6) registra y sigue: cuenta el fallo y continúa la iteración y la corrida. Perfecto para pruebas de carga, donde quieres saber cuántas respuestas salieron mal de entre miles, bajo presión.
¿Y si quieres que la prueba falle de verdad —que devuelva un código de error, que gatee un deploy— cuando demasiados checks fallan? Para eso están los thresholds, el tema del módulo 5. Un threshold puede decir "si más del 1 % de los checks falla, la prueba falla". El check mide; el threshold decide. En este módulo nos quedamos en la medición: contar checks. Recordarlo evita el error de esperar que un check fallido tumbe la corrida —no lo hace, y es a propósito—.
Errores comunes
Esperar que un check fallido aborte la prueba. Qué pasa: alguien pone un check, corre la prueba, ve que un criterio falla, y se sorprende de que la corrida haya seguido hasta el final "como si nada". Por qué pasa: trae el modelo mental del assert, que sí detiene todo. Cómo detectarlo: el resumen muestra checks fallidos (por ejemplo 76.67 %) pero la corrida completó todas sus iteraciones y salió con éxito. Cómo corregirlo: entiende que check mide, no aborta. Si necesitas que la prueba falle al superar un umbral de fallos, eso es un threshold (módulo 5), no un check.
Verificar solo el status y no el contenido. Qué pasa: alguien pone únicamente 'status is 200' y confía en que con eso basta. Por qué pasa: un 200 se siente como "todo bien". Cómo detectarlo: como en la corrida con bug —status 100 % verde pero precios equivocados—, la prueba pasa el status y no nota que la lógica está rota. Cómo corregirlo: verifica también el valor de la respuesta (price_cents === 7500). El status dice que la API respondió; el contenido dice que respondió bien. Bajo carga, un sistema estresado puede responder 200 y calcular mal.
Nombres de check vagos. Qué pasa: alguien nombra sus criterios 'check1', 'ok', 'test'. Por qué pasa: al escribirlos parece que el nombre no importa. Cómo detectarlo: cuando un check falla, el resumen dice ✗ check1 y no tienes idea de qué se rompió. Cómo corregirlo: nombra cada criterio por lo que verifica —'status is 200', 'price is 7500', 'booking confirmed'—. El nombre es lo que leerás en el reporte; que sea descriptivo es lo que hace útil el diagnóstico.
Ejercicios
Ejercicio 1 — Escribe los checks. Escribe (en k6, como contenido) un check para una respuesta de POST /book que verifique tres cosas: que el status es 200, que la respuesta trae confirmed: true, y que el booking_id no está vacío. 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 is present': (r) => r.json('booking_id') !== '',
});
- Cada clave es un nombre descriptivo que aparecerá en el resumen.
r.json('confirmed') === trueverifica el contenido booleano.r.json('booking_id') !== ''comprueba que el id no viene vacío. (Podrías afinarlo, por ejemplo, comprobando que empieza con'bk-'.)
Ejercicio 2 — Lee el conteo. En la corrida con bug, el resumen dijo checks: 76.67% (69 de 90), con status is 200: 45 ok / 0 fail y price is correct: 24 ok / 21 fail. (a) ¿Cuántos checks se ejecutan por iteración y cuántas iteraciones hubo? (b) ¿Por qué el status quedó en 100 % pero los checks totales bajaron a 76.67 %? (c) ¿Qué habrías dejado de ver si solo hubieras verificado el status?
Ver solución
- (a) 2 checks por iteración (status y precio). Hubo 45 iteraciones: 45 × 2 = 90 checks totales, que es el "de 90" del resumen.
- (b) Porque el fallo estaba solo en el criterio de precio, no en el de status. El status pasó las 45 veces (45/45), pero el precio falló 21 veces (24/45). Sumando ambos criterios: 45 + 24 = 69 checks buenos de 90 = 76.67 %.
- (c) Habrías dejado de ver el bug por completo: el status quedó en 100 % verde. Solo el check de contenido (el precio) reveló que ~la mitad de las cotizaciones
protraían el valor equivocado. Verificar solo el status te habría dado una falsa sensación de que todo estaba bien.
Ejercicio 3 — check vs assert. Un compañero viene de escribir tests unitarios con assert y dice: "en mi prueba de carga puse un check, pero cuando la API devolvió un precio malo, la prueba siguió corriendo en vez de detenerse. ¿Está roto k6?" Explícale qué pasa y cómo lograría que la prueba falle si demasiadas respuestas salen mal.
Ver solución
k6 no está roto: así funciona check a propósito. A diferencia de un assert —que aborta al primer fallo—, un check registra el fallo y sigue, porque en una prueba de carga quieres medir cuántas de entre miles de respuestas salieron mal, no detenerte en la primera. Por eso la corrida completó sus iteraciones y el resumen mostró el porcentaje de checks que fallaron.
Para que la prueba falle de verdad (por ejemplo, que devuelva un código de error y gatee un deploy) cuando demasiadas respuestas salen mal, necesita un threshold —el tema del módulo 5—: algo como "si la tasa de checks fallidos supera el 1 %, la prueba falla". El check mide; el threshold decide el veredicto.
Resumen y siguiente paso
En esta lección el VU pasó de leer la respuesta a verificarla. check(res, { ... }) evalúa criterios con nombre —"status is 200", "price is 7500"— y cuenta cuántos pasan y cuántos fallan, sin abortar la prueba (a diferencia de un assert): es un inspector de calidad que reporta el porcentaje de piezas malas sin parar la línea. Lo ejecutaste de verdad en Python: con la API sana, 3 VUs / 3 s dieron 18 de 18 checks (100 %); con un bug que olvidaba el descuento pro, el check de precio atrapó la discrepancia (24 ok / 21 fail, 76.67 % total) mientras el status seguía en verde —la prueba de que verificar solo el status no basta, y que un check de contenido es lo que descubre los bugs de lógica bajo carga—.
Antes de avanzar deberías poder: escribir un check con varios criterios y nombres descriptivos; verificar tanto el status como el valor de la respuesta; leer el conteo de checks del resumen; y explicar por qué un check fallido no detiene la corrida y qué (un threshold) sí la haría fallar.
La lección 5 vuelve sobre una línea que hemos usado sin explicar del todo: sleep(), el think time. Verás por qué, sin esa pausa, tu prueba mide una tormenta irreal en vez de un uso realista —y lo comprobarás con el mismo VU corriendo con y sin pausa, midiendo cómo el think time gobierna el ritmo—.
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/http—res.statusyres.json(), lo que los criterios del check inspeccionan. Cómo se lee el contenido a verificar. - Thresholds en k6 — cómo convertir la tasa de checks fallidos en un veredicto que hace PASAR o FALLAR la prueba. El "después" del check, a fondo en el módulo 5.
assert— Documentación de Python — la sentencia que sí aborta al primer fallo, para contrastar con la filosofía "medir sin detener" delcheck.