Módulo 6: Checks, groups y escenarios realistas
3. Un check que falla vs un threshold que aborta
Descripción
Hay una confusión que casi todo el mundo trae de los tests unitarios y que conviene desmontar de una vez: creer que un check que falla detiene la prueba. No lo hace. Un check que falla registra el fallo y sigue —la iteración termina sus pasos, los demás VUs siguen golpeando, la corrida completa su duración—. Esto no es un defecto: es exactamente lo que quieres en una prueba de carga, donde te interesa cuántas de entre miles de respuestas salieron mal, no frenar en la primera. Pero deja una pregunta abierta: si el check no detiene nada, ¿quién decide que la prueba falló? ¿Quién hace que el pipeline se ponga rojo cuando demasiadas respuestas están mal?
La respuesta es el threshold, que conociste en el módulo 5. Y la distinción entre los dos es el tema de esta lección, porque es la que organiza todo el veredicto de una prueba de carga: el check mide, el threshold decide. Un check produce un número —la tasa de checks que pasaron, por ejemplo 94.10 %—. Un threshold toma ese número y lo compara contra un umbral —"la tasa de checks fallidos debe ser menor al 1 %"— y de esa comparación sale un veredicto binario: PASS o FAIL, con un exit code (0 o distinto de 0) que un sistema de CI puede leer para dejar pasar o bloquear un deploy. El check nunca aborta; el threshold es el único que convierte fallos en un veredicto.
Conexión con el módulo: esta lección conecta la pieza nueva (el check de corrección de la lección 2) con una pieza ya aprendida (el threshold del módulo 5, que aquí reusamos, no reexplicamos). El check de k6 va como contenido; su comportamiento —falla y sigue— lo ejecutamos de verdad con la corrida de un check de precio con bug, y el gate de threshold —evaluar la tasa y devolver PASS/FAIL + exit code— también se ejecuta en Python, como espejo del threshold de k6. Verás las dos cosas medidas: la corrida completa sus iteraciones a pesar de los fallos, y el gate produce un exit code real. Todo lo rotulado como Python fue medido con Python 3.14.0.
El detector de humo y el interruptor de la alarma
Piénsalo con dos aparatos de una casa. El detector de humo hace una cosa: detecta y cuenta. Cada vez que hay humo, se enciende y lo registra. No apaga el fuego, no llama a los bomberos, no corta la luz —solo detecta—. Es deliberadamente pasivo: su trabajo es observar y avisar, no actuar. Si detectara y actuara a la vez, un poco de humo de la cocina cortaría la luz de toda la casa, y eso sería peor que el problema.
El interruptor de la alarma general es lo que actúa: cuando el detector ha registrado suficiente humo —pasa un umbral—, la alarma se dispara, se avisa a los bomberos, se toma una decisión. El detector alimenta a la alarma con datos; la alarma decide cuándo el nivel de humo amerita una acción. Separar los dos es lo que hace el sistema útil: el detector puede ser sensible y contar cada wisp de humo sin causar caos, porque no es él quien decide; y la alarma puede tener un umbral bien pensado ("si el humo pasa de X durante Y segundos") porque no tiene que detectar, solo decidir sobre lo que el detector contó.
En una prueba de carga, el check es el detector de humo: detecta respuestas malas y las cuenta, sin abortar nada. El threshold es el interruptor de la alarma: toma la cuenta del check y decide, contra un umbral, si la prueba pasa o falla. Por eso el check nunca detiene la corrida —sería el detector cortando la luz— y por eso el threshold existe aparte —es quien tiene la autoridad de dar el veredicto—.
El check detecta y cuenta respuestas malas sin abortar nada (el detector de humo). El threshold toma esa cuenta y decide, contra un umbral, si la prueba pasa o falla, con un exit code (el interruptor de la alarma). El check mide; el threshold decide. Separarlos es lo que permite que el check sea sensible sin causar caos y que el veredicto sea deliberado.
Cómo se ven las dos cosas en k6 (contenido)
En k6, el check y el threshold viven en lugares distintos del script, lo cual refuerza que hacen cosas distintas. El check va dentro de la función del VU (detecta por cada respuesta); el threshold va en options (decide sobre el total):
// check_vs_threshold.js - el check mide dentro del VU; el threshold decide en options.
// MOSTRADO COMO CONTENIDO: k6 no esta instalado en este entorno.
import http from 'k6/http';
import { check } from 'k6';
export const options = {
// EL THRESHOLD (decide): la tasa de checks que pasan debe ser > 99%.
// Si baja de ahi, k6 marca la prueba como FALLIDA y sale con codigo != 0.
thresholds: {
checks: ['rate>0.99'],
},
};
const BASE_URL = 'http://localhost:8000';
export default function () {
const payload = JSON.stringify({ room: 'Focus', tier: 'pro', hours: 3 });
const params = { headers: { 'Content-Type': 'application/json' } };
const res = http.post(`${BASE_URL}/quote`, payload, params);
// EL CHECK (mide): detecta y cuenta, NO aborta. Sigue aunque falle.
check(res, {
'status is 200': (r) => r.status === 200,
'price is correct': (r) => r.json('price_cents') === 6000, // Focus/pro/3h
});
}
Las dos piezas, y cómo se relacionan:
- El
check(dentro del VU) evalúa sus criterios por cada respuesta y alimenta una métrica interna de k6 llamadachecks—la tasa de checks que pasaron—. Nunca detiene la iteración; solo cuenta. - El
thresholdchecks: ['rate>0.99'](enoptions) lee esa métricachecksy exige que su tasa sea mayor a0.99(99 %). Si al final de la prueba la tasa es menor, k6 marca la prueba como fallida y sale con un exit code distinto de 0 —la señal que un CI usa para bloquear un deploy—.
El puente entre ambos es la métrica checks: el check la alimenta, el threshold la juzga. Un check sin threshold mide pero nunca hace fallar la prueba; un threshold sin nada que medir no tiene sentido. Juntos son el detector y la alarma.
Ver el check fallar sin abortar (ejecutado)
Provoquemos fallos con sentido para ver el comportamiento "mide y sigue". Corramos el escenario de la lección 2 con una variante: el check de 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. El check de precio debería fallar en todas las cotizaciones pro, mientras los demás criterios siguen verdes. Lo importante: la corrida debe completar todas sus iteraciones a pesar de los fallos.
Primero, la corrida sana (sin bug), 4 VUs / 3 s / think 100 ms, para tener la referencia. 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
...
http_errors....: 0
----------------------------------------------------------------
100 % de checks: la API sana pasa los 8 criterios por iteración (872 = 109 × 8). Ahora la corrida con el bug en el check de precio, misma configuración:
Qué esperar. El check de precio de la cotización fallará en las filas pro (la mitad del dataset), así que la tasa total bajará; pero la corrida debe completar sus ~108 iteraciones igual, sin detenerse. Salida real en este entorno:
----------------------------------------------------------------
Escenario cotizar->reservar->confirmar -> 4 VUs / 3s / think 100ms (BUG en el check de precio)
----------------------------------------------------------------
vus............: 4
iterations.....: 108 (34.4/s)
checks.........: 94.10% (813 de 864)
quote: status is 200..............: 108 ok / 0 fail
quote: price is correct...........: 57 ok / 51 fail <-- FALLA
book: status is 200...............: 108 ok / 0 fail
book: confirmed is true...........: 108 ok / 0 fail
book: booking_id present..........: 108 ok / 0 fail
confirm: status is 200............: 108 ok / 0 fail
confirm: id matches...............: 108 ok / 0 fail
confirm: price matches quote......: 108 ok / 0 fail
http_errors....: 0
----------------------------------------------------------------
Léela con cuidado, porque enseña el comportamiento clave:
iterations: 108— la corrida completó sus 108 iteraciones a pesar de los 51 checks fallidos. No se detuvo en el primer fallo. Esto es lo esencial: el check falló muchas veces y la prueba siguió midiendo hasta el final. Unasserthabría abortado en el primerpro.quote: price is correct: 57 ok / 51 fail— el check de precio atrapó las cotizacionespro. De 108 iteraciones, ~57 pidieron filasbasic(cuadran) y ~51 pidieron filaspro(fallan, porque el esperado buggy olvidó el descuento). El check detectó y contó, sin abortar.checks: 94.10% (813 de 864)— de 864 checks totales (108 × 8 criterios), 813 pasaron y 51 fallaron. El porcentaje bajó de 100 % a 94.10 %. Ese número es lo que el check produce: una medición. Pero fíjate: la corrida salió con éxito de todas formas. El check midió el problema, pero por sí solo no hizo fallar la prueba.
Y ahí está la pregunta que abre la segunda mitad: 94.10 % de checks es claramente malo, pero la prueba "pasó" (completó, sin error). ¿Quién convierte ese 94.10 % en un FAIL?
Ver el threshold decidir (ejecutado)
El threshold. Reusando la idea del módulo 5, escribimos un gate que toma la tasa de checks y la evalúa contra un umbral —"menos del 1 % de checks fallidos"—, devolviendo PASS/FAIL y un exit code, exactamente como haría el threshold checks: ['rate>0.99'] de k6:
# threshold_gate.py - el veredicto que el check NO da: un threshold sobre la
# tasa de checks fallidos (espejo del threshold de k6, tema de M5). SE EJECUTA.
import sys
passed, total, max_fail = int(sys.argv[1]), int(sys.argv[2]), float(sys.argv[3])
fail_rate = (total - passed) / total if total else 0.0
ok = fail_rate < max_fail # la DECISION
verdict = "PASS" if ok else "FAIL"
print(f" checks: {passed}/{total} tasa de fallo: {fail_rate*100:.2f}% "
f"umbral: <{max_fail*100:.2f}%")
print(f" threshold checks:['rate>{1-max_fail:.2f}'] -> {verdict} (exit {0 if ok else 1})")
sys.exit(0 if ok else 1) # exit code que un CI lee
Le pasamos los números reales de las dos corridas de arriba y miramos el veredicto. Primero la sana (872/872), luego la del bug (813/864), contra el mismo umbral del 1 %:
Qué esperar. La corrida sana (0 % de fallos) debe pasar el umbral (exit 0); la del bug (5.90 % de fallos, muy por encima del 1 %) debe fallarlo (exit 1). Salida real en este entorno:
### Corrida sana (872/872) contra threshold 'menos de 1% de checks fallidos':
checks: 872/872 tasa de fallo: 0.00% umbral: <1.00%
threshold checks:['rate>0.99'] -> PASS (exit 0)
[exit code real: 0]
### Corrida con bug (813/864) contra el MISMO threshold:
checks: 813/864 tasa de fallo: 5.90% umbral: <1.00%
threshold checks:['rate>0.99'] -> FAIL (exit 1)
[exit code real: 1]
Aquí está la decisión, medida:
- La corrida sana → PASS, exit 0. 0 % de checks fallidos está por debajo del umbral del 1 %. El threshold da luz verde. Un CI que lea
exit 0deja pasar el deploy. - La corrida con bug → FAIL, exit 1. 5.90 % de checks fallidos supera el umbral del 1 %. El threshold da luz roja y el exit code es 1. Un CI que lea
exit 1bloquea el deploy. Fíjate: el 94.10 % de checks era exactamente el mismo número que produjo la corrida con bug; el threshold no lo cambió, solo lo juzgó. - El puente completo. El check midió (94.10 %). El threshold decidió (FAIL, exit 1). Ni el check hizo fallar la prueba por sí solo (la corrida completó con éxito), ni el threshold midió nada (solo leyó el número del check). Detector y alarma, cada uno en su papel.
Esto es el veredicto de una prueba de carga en una frase: los checks cuentan las respuestas malas durante toda la corrida sin detenerla; al final, un threshold sobre la tasa de checks convierte ese conteo en el PASS/FAIL que gatea (o no) el deploy. El exit 1 es la señal mecánica que hace que un pipeline se ponga rojo.
Errores comunes
Esperar que un check fallido aborte la iteración o la prueba. Qué pasa: alguien ve checks fallidos y se sorprende de que la corrida haya completado "como si nada". Por qué pasa: trae el modelo del assert, que aborta al primer fallo. Cómo detectarlo: el resumen muestra checks fallidos (94.10 %) pero la corrida terminó todas sus iteraciones y salió con éxito. Cómo corregirlo: entiende que el check mide, no aborta. Si necesitas que la prueba falle, pon un threshold sobre la tasa de checks. El check es el detector; el threshold es la alarma.
Poner checks pero ningún threshold, y esperar que el CI se ponga rojo. Qué pasa: alguien llena el script de checks, algunos fallan, pero el pipeline sigue verde y el deploy pasa. Por qué pasa: los checks miden, pero sin un threshold nadie convierte la medición en un veredicto —k6 sale con código 0 aunque haya checks fallidos—. Cómo detectarlo: checks al 94 % y sin embargo exit 0. Cómo corregirlo: añade thresholds: { checks: ['rate>0.99'] } (o el umbral que decidas) en options. Sin threshold, los checks son un detector de humo desconectado de cualquier alarma.
Poner el umbral del threshold en el lugar equivocado del script. Qué pasa: alguien intenta meter la lógica del threshold dentro de la función del VU, o el check dentro de options. Por qué pasa: no tiene clara la división de trabajo. Cómo detectarlo: el script no compila o no hace lo esperado. Cómo corregirlo: recuerda dónde vive cada uno. El check va dentro de la función del VU (mide por respuesta); el threshold va en options (decide sobre el total). El lugar refleja el papel: detectar es por respuesta, decidir es sobre el agregado.
Ejercicios
Ejercicio 1 — ¿Mide o decide? Para cada afirmación, di si describe un check o un threshold. (a) "Cuenta cuántas respuestas tuvieron el precio correcto." (b) "Hace que la prueba salga con exit code 1 si más del 1 % de las respuestas falla." (c) "Va dentro de la función del VU y no aborta nada." (d) "Va en options y produce el veredicto pasa/falla."
Ver solución
- (a) Check — cuenta (mide) respuestas correctas.
- (b) Threshold — produce el exit code (decide).
- (c) Check — vive dentro del VU y no aborta.
- (d) Threshold — vive en
optionsy da el veredicto.
La regla: si cuenta y no aborta, es check; si decide pasa/falla con exit code, es threshold.
Ejercicio 2 — Del conteo al veredicto. En la corrida con bug, el resumen dio checks: 94.10% (813 de 864). (a) ¿Cuál es la tasa de checks fallidos? (b) ¿Pasa o falla contra un threshold checks: ['rate>0.99']? (c) ¿Y contra uno más laxo, checks: ['rate>0.90']? Justifica cada uno.
Ver solución
- (a) Fallidos: 864 − 813 = 51 de 864 = 5.90 %. (La tasa de pasados es 94.10 %.)
- (b) Falla. El threshold exige una tasa de pasados > 99 % (equivalente a < 1 % de fallos). Con 94.10 % de pasados (5.90 % de fallos), no llega: FAIL, exit 1.
- (c) Pasa. El threshold laxo exige > 90 % de pasados. Con 94.10 %, sí supera el 90 %: PASS, exit 0. Mismo conteo del check, distinto umbral, distinto veredicto —lo cual muestra que el threshold es una decisión de política separada de la medición—.
Ejercicio 3 — El compañero confundido. Un compañero dice: "Puse un check de precio en mi prueba de carga. La API devolvió precios malos como la mitad de las veces, pero la prueba corrió hasta el final y k6 salió con código 0, así que mi CI dejó pasar el deploy. ¿Está roto el check?" Explícale qué pasó y qué le falta.
Ver solución
El check no está roto: hizo su trabajo, que es medir. Detectó y contó los precios malos (la tasa de checks bajó), pero un check —por diseño— no aborta ni hace fallar la prueba; solo cuenta. Por eso la corrida completó y, sin nada más, k6 salió con código 0.
Lo que le falta es un threshold sobre la tasa de checks, por ejemplo thresholds: { checks: ['rate>0.99'] } en options. Ese threshold leería la tasa que el check produjo y, al ver que la mitad falló, marcaría la prueba como fallida y saldría con exit code != 0, que es la señal que hace que el CI ponga el pipeline en rojo y bloquee el deploy. El check es el detector de humo; el threshold es la alarma. Necesita conectar los dos.
Resumen y siguiente paso
En esta lección quedó nítida la división de trabajo que organiza el veredicto de una prueba de carga: el check mide, el threshold decide. Un check que falla registra y sigue —no aborta la iteración ni la prueba—, y lo viste medido: la corrida con un check de precio con bug completó sus 108 iteraciones a pesar de 51 fallos, con la tasa de checks en 94.10 %. Ese número es una medición, no un veredicto. El threshold —reusado del módulo 5— es quien lo convierte en veredicto: le pasaste los números reales a un gate con umbral del 1 %, y la corrida sana salió PASS (exit 0) mientras la del bug salió FAIL (exit 1), la señal que un CI lee para bloquear un deploy. Detector de humo y alarma: sensible el uno, deliberada la otra.
Antes de avanzar deberías poder: explicar por qué un check fallido no detiene la corrida y por qué eso es deseable; distinguir qué mide un check de qué decide un threshold; saber dónde vive cada uno en el script de k6 (check en el VU, threshold en options); y calcular si una tasa de checks dada pasa o falla contra un umbral.
La lección 4 organiza el escenario que hemos empezado a correr. Cuando una iteración tiene varios pasos —cotizar, reservar, confirmar—, conviene envolverlos en bloques con nombre: los group() de k6. Verás cómo agrupar da métricas por paso (cuánto tardó cotizar vs reservar vs confirmar) y etiqueta los checks por grupo —y lo medirás en Python, que ya reporta la latencia por grupo del escenario—.
Recursos
- Checks en k6 — la referencia de
check()y su métricachecks, y la confirmación de que un check fallido no aborta la prueba. La mitad "mide" de esta lección. - Thresholds en k6 — cómo
thresholds: { checks: ['rate>0.99'] }convierte la tasa de checks en un veredicto pasa/falla con exit code. La mitad "decide". Reusado del módulo 5. - Módulo 5 de esta guía — Thresholds, pasa/falla y SLOs — dónde se enseña a fondo el threshold que aquí solo reusamos. Repásalo si no tienes claro cómo un umbral gatea un deploy.
sys.exit— documentación de Python — cómo un programa devuelve un exit code (0 o distinto de 0), la señal que un CI lee. El mecanismo del gate que ejecutaste.