Módulo 5: Thresholds — pasa/falla y SLOs

6. El gate de rendimiento, como los coverage gates

Descripción

Hasta aquí construimos un gate de rendimiento pieza por pieza: una métrica (p95), un umbral (200 ms), un veredicto (pasa/falla), un código de salida (0 o ≠ 0) que gatea el deploy. En esta lección damos un paso atrás y vemos algo liberador: ese patrón no es exclusivo del rendimiento. Es la anatomía de todos los quality gates, y tú ya conociste otro miembro de la familia en el ecosistema Testing —el coverage gate—. Un coverage gate mide la cobertura de pruebas (un %), la compara con un umbral (80%), y hace fallar el build si no llega, saliendo con un código ≠ 0. Es, línea por línea, la misma máquina que tu gate de rendimiento, aplicada a otra dimensión de la calidad. Ver los dos lado a lado te da una intuición que trasciende esta guía: cualquier propiedad medible de tu software puede volverse un gate —cobertura, latencia, tamaño del bundle, número de vulnerabilidades, deuda de accesibilidad—, todas con la misma receta. Al terminar entenderás el rendimiento no como un tema aislado, sino como un gate más en el tablero de calidad de tu pipeline, y sabrás dónde encaja en la pirámide de pruebas.

Conexión con el módulo: las lecciones 2–5 construyeron y justificaron el gate de rendimiento en concreto. Esta lo generaliza: muestra que es una instancia de un patrón que ya usas en otras familias de pruebas, y enlaza explícitamente con la guía testing-fundamentals-and-tdd-guide, donde el coverage gate se enseña a fondo. Es la lección más conceptual del módulo y la que conecta esta guía con el resto del ecosistema Testing. Lo que sigue (lección 7) vuelve a lo concreto de k6 con abortOnFail y los thresholds por escenario; el módulo 7 instala este gate en el pipeline de CI completo.

La misma báscula, distintas cosas que pesar

Vuelve a la estación de calidad de la fábrica —la que pesa botellas—. Ahora imagina que la misma línea produce, además de botellas, cajas de cartón. Al final de la línea pones otra estación idéntica en su mecánica: mide una propiedad (esta vez el grosor del cartón, no el peso del líquido), la compara con una regla ("al menos 3 mm"), y enciende una luz verde o roja que dispara el brazo mecánico. La estación de las botellas y la de las cajas son la misma máquina —báscula, regla, luz, brazo—; lo único distinto es *qué_propiedad_ miden y *qué_umbral_ usan. Si entiendes una, entiendes la otra, porque comparten la anatomía completa.

Los quality gates de tu pipeline son estaciones así. El coverage gate mide "qué porcentaje del código ejecutan las pruebas"; tu gate de rendimiento mide "cuánto tarda el p95 bajo carga". Distintas propiedades, distintos umbrales, idéntica máquina: medir → comparar → veredicto binario → código de salida → el pipeline actúa. Esta lección es sobre reconocer esa máquina compartida, para que no tengas que reaprenderla cada vez que quieras gatear una dimensión nueva de la calidad.

El coverage gate, de cerca

En la guía de fundamentos de pruebas aprendiste sobre cobertura: qué porcentaje de tus líneas de código ejecutan tus tests. Y aprendiste una advertencia importante —la cobertura es una herramienta, no una meta: 100% de cobertura no significa 0 bugs, y perseguir el número por el número lleva a tests inútiles—. Con esa madurez, la cobertura se puede convertir en un gate: no para adorar el número, sino para evitar regresiones —que un cambio deje sin probar código que antes sí se probaba—.

En pytest con el plugin de cobertura, ese gate es una sola bandera:

# CONTENIDO (referencia al ecosistema Testing): el coverage gate.
$ pytest --cov=reservo --cov-fail-under=80
...
Required test coverage of 80% not reached. Total coverage: 73.20%
$ echo $?
1

Míralo con los ojos de este módulo. --cov=reservo dice qué medir (la cobertura del paquete reservo) —es el SLI—. --cov-fail-under=80 es el umbral —"la cobertura debe estar por encima del 80%", un SLO—. La cobertura medida (73.20%) no llega, así que pytest emite el veredicto (reprobado) y sale con código 1 —el mismo sello de "retenido" de la lección 4—. Un pipeline que corre este comando pone el build en rojo exactamente igual que con un test roto o con un threshold de k6 fallado. Es tu gate de rendimiento con otra métrica y otro umbral.

El mapeo, lado a lado

Pongamos las dos estaciones una junto a la otra, con la anatomía que ya conoces:

Parte del gateCoverage gate (Testing)Gate de rendimiento (esta guía)
Qué se mide (SLI)Cobertura de pruebas (%)Latencia p95, tasa de error, checks
El umbral (SLO)--cov-fail-under=80p(95)<200, rate<0.01, rate>0.99
Cómo se midepytest --covk6 (contenido) / generador Python (ejecutado)
El veredictocobertura ≥ 80% ? pasa : fallathresholds cumplidos ? pasa : falla
El código de salidapytest sale con ≠ 0 si no llegak6 sale con 99 / el gate Python con 1
Consecuenciabuild rojo, merge bloqueadobuild rojo, deploy bloqueado
La advertenciacobertura alta ≠ 0 bugslatencia baja ≠ app buena para todo

Cada fila es idéntica en estructura; solo cambia el contenido de las dos columnas. Y fíjate en la última fila —la advertencia—, porque revela que la familia comparte hasta sus trampas. Así como una cobertura del 100% no garantiza ausencia de bugs (puedes ejecutar cada línea sin verificar nada útil), un p95 bajo no garantiza una app buena (puede ser rápida y devolver errores, o rápida solo en el endpoint que mediste). En ambos casos, el gate protege contra una dimensión de la calidad, y confundir "pasó el gate" con "el software es bueno" es el mismo error en las dos familias. Un gate es una condición necesaria, no suficiente.

Por qué el rendimiento merece su propio gate

Si el patrón es el mismo, ¿por qué no basta con los gates que ya tienes (tests, cobertura, linter)? Porque cada gate protege una dimensión distinta, y ninguna cubre a las demás. Tus tests unitarios verifican que la lógica es correcta —que price_cents(Focus, basic, 3) da 7500—, pero corren con una petición a la vez: no dicen nada de qué pasa con 120 peticiones concurrentes. El coverage gate verifica que tus tests tocan el código, pero no que el código sea rápido. El linter verifica el estilo. Ninguno mira la latencia bajo carga. El rendimiento es una propiedad ortogonal a la corrección, la cobertura y el estilo: un código puede ser correcto, bien probado, bien formateado —y lento bajo carga—. Por eso necesita su propia estación en la línea.

Esto conecta con la pirámide de pruebas que estructura todo el ecosistema Testing. En la base están los tests unitarios (muchos, rápidos, sobre unidades de lógica); en medio, los de integración; arriba, los E2E (pocos, lentos, sobre flujos completos por el navegador —la guía de Playwright—). Las pruebas de carga son la otra punta de la pirámide, junto a los E2E: pocas, caras de correr, sobre el sistema entero —pero preguntando algo que ninguna otra capa pregunta: no "¿es correcto?" sino "¿aguanta?"—. El gate de rendimiento es cómo esa punta de la pirámide se vuelve automática, igual que el coverage gate automatiza la vigilancia de la base.

La generalización: cualquier cosa medible puede ser un gate

Una vez que ves la máquina, la ves en todas partes. Estos son todos gates con la misma anatomía, cada uno protegiendo una dimensión distinta:

  • Tamaño del bundle: medir los KB del JavaScript enviado al navegador, gatear en "< 250 KB", fallar el build si engorda. (Protege la velocidad de carga.)
  • Vulnerabilidades: medir cuántas dependencias tienen CVEs conocidos, gatear en "0 críticas", fallar si aparece una. (Protege la seguridad.)
  • Accesibilidad: medir cuántas violaciones de reglas WCAG tiene la página, gatear en "0 graves", fallar si hay. (Protege la inclusión.)
  • Rendimiento (esta guía): medir el p95 bajo carga, gatear en "< 200 ms", fallar si degrada. (Protege la experiencia bajo tráfico.)

Todos son: medir una propiedad → compararla con un umbral → veredicto binario → código de salida → el pipeline actúa. Aprendiste esa receta construyendo un gate de rendimiento con las manos en Python; ahora sabes que es transferible a cualquier propiedad que puedas medir y para la que puedas defender un umbral. Ese es el regalo conceptual de este módulo: no aprendiste "cómo poner un threshold en k6", aprendiste cómo se convierte cualquier dimensión de calidad en una compuerta automática.

Errores comunes

Creer que pasar un gate significa "el software es bueno". Qué pasa: el build está todo verde (tests, cobertura, rendimiento) y alguien concluye "está perfecto". Por qué pasa: se confunde "pasó las condiciones necesarias" con "cumple todas las propiedades deseables". Cómo detectarlo: si tratas el verde como garantía total en vez de como "no rompió estas dimensiones concretas", te vas a llevar sorpresas. Cómo corregirlo: entiende cada gate como protección de una dimensión; el conjunto de gates cubre lo que decidiste vigilar, no todo lo imaginable. Un gate es necesario, no suficiente.

Duplicar en un gate lo que otro ya cubre (y dejar huecos). Qué pasa: un equipo tiene tres gates que verifican variantes de lo mismo (corrección) y ninguno que verifique el rendimiento. Por qué pasa: se añaden gates por inercia sin mapear qué dimensión cubre cada uno. Cómo detectarlo: lista tus gates y la dimensión que protege cada uno; si hay dimensiones importantes sin gate (como el rendimiento), tienes un hueco. Cómo corregirlo: piensa en dimensiones ortogonales —corrección, cobertura, rendimiento, seguridad— y pon un gate por la que te importe, sin redundar.

Perseguir el número del gate en vez de lo que representa. Qué pasa: igual que alguien infla la cobertura al 100% con tests que no verifican nada, alguien "optimiza" para pasar el gate de rendimiento midiendo solo el endpoint rápido y evitando el lento. Por qué pasa: el gate se vuelve la meta, no el medio. Cómo detectarlo: si tu prueba de carga evita a propósito los caminos que sospechas lentos, estás gameando el gate. Cómo corregirlo: recuerda la advertencia compartida de la familia —el número es una herramienta, no la meta—; mide los caminos que de verdad importan al usuario, aunque salgan feos.

Ejercicios

Ejercicio 1 — Reconoce la máquina. Para cada gate, identifica sus cinco partes (qué mide, umbral, cómo mide, veredicto, consecuencia). (a) pytest --cov-fail-under=80. (b) http_req_duration: ["p(95)<200"] en k6. (c) Un gate de tamaño de bundle que falla si el JS supera 250 KB.

Ver solución
  • (a) Mide: cobertura de pruebas (%). Umbral: 80%. Cómo: pytest --cov. Veredicto: cobertura ≥ 80 ? pasa : falla. Consecuencia: pytest sale con ≠ 0 → build rojo → merge bloqueado.
  • (b) Mide: latencia p95. Umbral: 200 ms. Cómo: k6 (o el generador Python). Veredicto: p95 < 200 ? pasa : falla. Consecuencia: k6 sale con 99 → build rojo → deploy bloqueado.
  • (c) Mide: KB del bundle JS. Umbral: 250 KB. Cómo: una herramienta que mide el build. Veredicto: KB ≤ 250 ? pasa : falla. Consecuencia: el paso sale con ≠ 0 → build rojo.

Los tres son la misma máquina con distinto contenido en "qué mide" y "umbral".

Ejercicio 2 — La advertencia compartida. La cobertura tiene la advertencia "100% ≠ 0 bugs". Formula la advertencia equivalente para el gate de rendimiento, y da un ejemplo concreto de una app que pasa el gate de p95 pero igual es mala.

Ver solución

La advertencia equivalente: "p95 bajo ≠ app buena" —cumplir el umbral de latencia no garantiza una buena experiencia—. Ejemplos concretos de una app que pasa p(95)<200 pero es mala: (a) responde en 8 ms pero el 8% de las respuestas son errores 500 (rápida y rota —por eso también gateamos http_req_failed); (b) el endpoint que mediste (/quote) es veloz, pero /book, que no incluiste en la prueba, tarda 3 s (mediste el camino equivocado); (c) es rápida con la carga que probaste pero colapsa con el doble (elegiste un perfil de carga que no refleja el pico real). En los tres, el gate está verde y la app es mala —el gate protege una dimensión concreta, no todo—.

Ejercicio 3 — Ubica en la pirámide. ¿Dónde vive una prueba de carga en la pirámide de pruebas, y qué pregunta responde que ninguna otra capa responde? Contrástala con los tests unitarios (base) y los E2E (punta).

Ver solución

Una prueba de carga vive en la punta de la pirámide, junto a los E2E: son pocas, caras de correr, y ejercen el sistema entero (no una unidad aislada). Pero responde una pregunta distinta a la de los E2E: los E2E preguntan "¿es correcto?" (el usuario que hace clic en Cotizar, ¿ve el precio bien?, por el navegador —la guía de Playwright—); la prueba de carga pregunta "¿aguanta?" (con 120 usuarios a la vez, ¿sigue respondiendo rápido y sin errores?). Los tests unitarios de la base verifican la lógica con una petición a la vez (price_cents da 7500) —correctos, rápidos, muchos— pero no dicen nada de la concurrencia. Ninguna otra capa pregunta por el comportamiento bajo carga; esa es la contribución única de la prueba de rendimiento a la pirámide, y el gate de este módulo es cómo se vuelve automática.

Resumen y siguiente paso

El gate de rendimiento no es una máquina única: es una instancia de la anatomía compartida por todos los quality gates —medir una propiedad, compararla con un umbral, emitir un veredicto binario, salir con un código que el pipeline respeta—. Lo viste al lado de su pariente cercano del ecosistema Testing, el coverage gate (pytest --cov-fail-under=80): misma máquina, distinta métrica (cobertura vs latencia) y distinto umbral (80% vs 200 ms), incluso la misma advertencia (cobertura alta ≠ 0 bugs; p95 bajo ≠ app buena). El rendimiento merece su propio gate porque es una dimensión ortogonal a la corrección, la cobertura y el estilo: un código puede ser correcto, bien probado y lento bajo carga. En la pirámide de pruebas, la carga vive en la punta junto a los E2E, pero preguntando "¿aguanta?" en vez de "¿es correcto?".

La generalización es el regalo del módulo: cualquier propiedad medible —tamaño de bundle, vulnerabilidades, accesibilidad, rendimiento— puede volverse un gate con la misma receta. No aprendiste solo a poner un threshold en k6; aprendiste a convertir cualquier dimensión de calidad en una compuerta automática.

Antes de avanzar deberías poder: describir la anatomía común de coverage gate y gate de rendimiento; explicar por qué el rendimiento necesita su propio gate (ortogonalidad); y ubicar la prueba de carga en la pirámide. Lo que sigue, en la lección 7, vuelve a lo concreto de k6 con dos refinamientos: abortOnFail (cortar temprano cuando un umbral crítico se rompe) y los thresholds por escenario (un SLO distinto para cada parte del sistema) —que es justo lo que aprendiste a valorar en la lección 5, ahora aplicado por partes—.

Recursos