Módulo 1: Por qué load y performance testing

1. Presentación del módulo: dos preguntas distintas

Descripción

Al terminar esta lección vas a tener clara la distinción que sostiene toda la guía, y que es más precisa de lo que suele contarse. Hay dos preguntas que le puedes hacer a un sistema, y son de naturaleza distinta. La primera es ¿funciona? —¿da la respuesta correcta cuando le pido una cosa concreta?—. La segunda es ¿aguanta? —¿sigue dando esa respuesta, y a tiempo, cuando no soy yo quien pregunta sino quinientas personas a la vez?—. La primera pregunta es de corrección; la responde el testing funcional (un test unitario, un test E2E). La segunda es de carga y rendimiento; la responde una prueba de carga, y es el tema de esta guía. Son dos ejes independientes: un sistema puede ser perfectamente correcto y a la vez colapsar con doscientos usuarios; puede ser rapidísimo y a la vez devolver el precio equivocado. Necesitas medir los dos, y con herramientas distintas.

Además vas a conocer Reservo, la API sobre la que trabajamos durante los ocho módulos: un servidor mínimo de reservas de salas de coworking, con tres endpoints —listar salas, cotizar un precio y reservar—. A diferencia de la guía hermana de Playwright, que prueba una página web por el navegador, aquí el sujeto de prueba es la API —el servidor HTTP que responde JSON—, porque la carga se mide contra el backend, que es donde el tráfico duele. Y —lo más importante— vas a conocer sus números-ancla: cotizar tres horas de la sala Focus con plan básico devuelve 7500 centavos; con plan pro, 6000. Esos dos números son el "checksum" de la guía: los vas a ver aparecer una y otra vez, y ya los ejecuté de verdad contra el servidor antes de escribirlos aquí.

Conexión con el módulo: esta lección es el mapa, no el territorio. Aquí todavía no escribes una prueba de carga completa —eso llega en orden: la lección 6 levanta la API, la 7 hace el primer contacto ejecutado, la 8 es el proyecto—. Lo que instalas aquí es la idea que hace que las otras siete tengan sentido: que "aguanta" es una pregunta distinta de "funciona", y que se responde midiendo. La lección 2 abre a fondo esa distinción funcional-vs-carga. La 3 nombra las tres preguntas concretas de una prueba de carga (usuarios, p95, punto de quiebre). La 4 presenta los cinco tipos (smoke, load, stress, spike, soak). La 5 explica qué es k6 y su runtime propio. La 6 construye y sirve la API de Reservo. La 7 hace el primer contacto —un script k6 como contenido y un generador Python ejecutado—. Y la 8, el proyecto, te pone a medir con tus manos.

Una nota sobre la frontera, porque marca el tono de toda la guía: aquí no probamos la corrección de la lógica de negocio. Que price_cents("Focus", "basic", 3) sea 7500 es un hecho que se verifica con un test funcional (unitario o E2E), y eso vive en otras guías del ecosistema Testing. Lo que hacemos aquí es dar por sentado que la lógica es correcta y preguntar otra cosa: cuando esa misma lógica correcta recibe cientos de peticiones por segundo, ¿cuánto tarda?, ¿cuántas aguanta antes de romperse?, ¿en qué punto se cae?. Mantener esa frontera clara —corrección allá, carga aquí— es lo que te deja aprender load testing sin confundirlo con el testing funcional que probablemente ya conoces.

El restaurante que sirve un plato perfecto... para una persona

Piénsalo con una cocina. Un chef prepara un risotto y lo cata: el punto del arroz, la sal, la temperatura. Está perfecto. Esa cata responde una pregunta —¿el plato está bien hecho?— y la responde con un sí rotundo. Es el testing funcional: fijas una entrada (esta receta, estos ingredientes) y verificas que la salida es correcta (un risotto en su punto). Puedes catar mil platos distintos y cada cata te dice si ese plato salió bien.

Pero ahora abre el restaurante un viernes a las nueve de la noche. Entran ochenta comensales en veinte minutos y todos piden risotto. El plato sigue siendo perfecto —la receta no cambió—, pero aparecen preguntas que la cata jamás hizo: ¿alcanzan las hornillas para ochenta risottos a la vez, o se forma una fila y los últimos platos salen fríos y con cuarenta minutos de espera? ¿En qué número de comensales la cocina deja de dar abasto —setenta, ochenta, cien—? Si de golpe entra un autobús de turistas y caen cincuenta pedidos en dos minutos, ¿la cocina absorbe el pico o colapsa? Esas son preguntas de carga y rendimiento. No preguntan si el plato está bien; preguntan si la cocina aguanta cuando la demandan en serio, y qué tan rápido sirve bajo esa presión.

Fíjate en que las dos clases de prueba son necesarias y ninguna sustituye a la otra. Un restaurante con un risotto perfecto que tarda cuarenta minutos en servirlo un viernes tiene un problema real —de rendimiento— que ninguna cata habría detectado, porque la cata siempre se hace con la cocina vacía. Y al revés: una cocina rapidísima que sirve un risotto salado tiene un problema de corrección que ninguna prueba de carga detectaría, porque la prueba de carga mide tiempos y volumen, no sabor. El chef necesita catar (funcional) y simular el viernes por la noche (carga). Toda esta guía es sobre lo segundo: cómo simular el viernes por la noche de tu API, medir qué pasa, y ponerle un umbral que diga "si el 95% de los platos no sale en menos de medio segundo, esto no está listo para abrir".

Conviene decirlo con todas las letras, porque es la idea que atraviesa los ocho módulos:

El testing funcional pregunta "¿da la respuesta correcta?" (corrección). El testing de carga y rendimiento pregunta "¿aguanta cuando muchos usuarios pegan a la vez, y qué tan rápido responde bajo presión?" (rendimiento). Son dos ejes independientes: necesitas los dos, y se miden con herramientas distintas. Esta guía es sobre el segundo.

La API de estudio de la guía: Reservo

Toda la guía trabaja sobre la misma API inventada, por la misma razón que un cocinero practica el servicio de un viernes siempre en la misma cocina: si cada lección estrenara un sistema nuevo, gastarías la mitad de tu atención entendiendo el contexto en lugar de aprendiendo a medir carga. Con una sola API, para el módulo 3 ya conoces Reservo de memoria y puedes concentrarte en lo que cada lección agrega —métricas, perfiles, thresholds— sin volver a preguntarte "espera, ¿qué devolvía este endpoint?".

Qué es Reservo y por qué es el blanco perfecto

Reservo es la API de reservas de salas de un espacio de coworking. Un coworking tiene salas de distintos tamaños —una cabina para llamadas (Focus), un estudio (Studio), una sala de juntas (Boardroom)— y los miembros las reservan por hora. La API de Reservo hace tres cosas: listar las salas y su tarifa, cotizar (dado sala, plan y horas, decir cuánto cuesta) y reservar (confirmar). Nada más. Es deliberadamente pequeña.

Lo que hace de Reservo el blanco perfecto para aprender load testing es justo lo que no tiene. No tiene base de datos, ni login, ni framework pesado: es un solo archivo de Python que usa http.server de la biblioteca estándar —el servidor HTTP más pequeño que existe, y que viene con Python—. ¿Por qué importa esa simpleza? Porque nos deja concentrarnos en la técnica de la prueba de carga —lanzar tráfico concurrente, medir latencia, calcular percentiles, poner umbrales— sin ahogarnos levantando infraestructura. Y a la vez tiene todo lo que una prueba de carga necesita practicar: endpoints HTTP reales, un GET y dos POST con cuerpo JSON, una respuesta con un número que podemos verificar. Es un blanco de carga en miniatura, pero completo.

Una honestidad por delante, en el espíritu de la guía: Reservo es una API de laboratorio. Corre en localhost, así que la red no es un factor y las latencias que mediremos serán muy bajas (milisegundos o fracciones). Una API real vive detrás de la red, con una base de datos que es casi siempre el verdadero cuello de botella, y sus latencias bajo carga serían mayores. Reservo se queda con el corazón —endpoints HTTP que responden bajo tráfico concurrente— y tira el resto, precisamente porque ese corazón es lo que hay que aprender a medir primero. La técnica que practiques aquí es idéntica a la que aplicarías contra una API de producción; solo cambian los números.

La API canónica: cómo se ve y qué contiene

Esta es la API que vas a construir en la lección 6 y medir el resto de la guía. Aquí presentamos su contrato —qué endpoints tiene y qué responde cada uno— para que la tengas de referencia desde el minuto uno. Los identificadores, campos y valores van en inglés (room, tier, hours, price_cents) porque así es el mercado tech real; la prosa que los explica va en español.

Método y rutaCuerpo de la peticiónRespuestaPara qué
GET /rooms{"rooms": [{"room": "Focus", "hourly_cents": 2500}, ...]}Listar las salas y su tarifa por hora (en centavos).
POST /quote{"room": "Focus", "tier": "basic", "hours": 3}{"price_cents": 7500}Cotizar: dado sala, plan y horas, devolver el precio en centavos.
POST /book{"room": "Focus", "tier": "basic", "hours": 3}{"booking_id": "bk_Focus_basic_3", "price_cents": 7500, "confirmed": true}Reservar: confirmar y devolver un identificador de reserva.

Las tarifas por hora, en centavos enteros (nunca float para dinero), son: Focus 2500, Studio 4000, Boardroom 8000. El precio de una cotización es tarifa × horas, y si el plan es pro se aplica un 20% de descuento enteroprecio * 80 // 100, con división entera, sin decimales—. No memorices el detalle; lo construimos con calma en la lección 6. Por ahora quédate con la forma: un GET para listar, un POST /quote que devuelve un price_cents, y un POST /book que confirma. Esos son los tres endpoints que el generador Python (ejecutado) y el script k6 (contenido) van a golpear con carga.

Los números-ancla

Aquí está el corazón numérico de la guía. Estos resultados exactos son los que la API devuelve, y los que vamos a verificar bajo carga módulo tras módulo. Son el "checksum": si la API los reproduce, la lógica está correcta y podemos concentrarnos en su rendimiento; si no, hay un bug de corrección (que es tema de otra guía). Los ejecuté de verdad contra el servidor de Reservo corriendo en localhost, con curl, antes de escribirlos aquí —no son inventados, son medidos—:

EndpointEntradaCuenta (centavos)Respuesta real
POST /quoteFocus / basic / 3h2500 × 3 = 7500{"price_cents": 7500}
POST /quoteFocus / pro / 3h7500 × 80 // 100 = 6000{"price_cents": 6000}
POST /quoteStudio / basic / 1h4000 × 1 = 4000{"price_cents": 4000}
POST /bookFocus / basic / 3h{"booking_id": "bk_Focus_basic_3", "price_cents": 7500, "confirmed": true}

Los dos primeros —7500 y 6000— son las anclas que más vas a ver. En una prueba de carga los usaremos de dos maneras: como el tráfico que lanzamos (miles de cotizaciones de Focus/basic/3h) y como el check de corrección bajo carga (verificar que, incluso con 500 peticiones concurrentes, la API sigue devolviendo 7500 y no un error). Que la respuesta siga siendo correcta bajo presión es en sí una cosa que la carga pone a prueba.

El mapa: las ocho lecciones y la guía

Este módulo va de entender por qué la carga es una pregunta distinta a conocer las herramientas y los tipos a levantar la API a medir por primera vez. Cada lección deja una pieza:

LecciónQué instala
1. Presentación (esta)La distinción funcional-vs-carga, la API Reservo, los números-ancla, el mapa
2. Funcional vs cargaLas dos preguntas a fondo: corrección vs rendimiento bajo presión
3. Las preguntas de una prueba de cargaUsuarios concurrentes, p95, punto de quiebre; por qué el promedio miente
4. Los tipos de pruebaSmoke, load, stress, spike, soak: qué pregunta cada uno
5. Qué es k6La herramienta, su runtime propio (no Node), su lugar
6. La API y servirlaConstruir el servidor de Reservo y verlo responder
7. Primer contactoUn script k6 (contenido) y un generador Python (ejecutado)
8. Mini-proyectoTu primera medición de carga real, con tus manos

Y la guía entera, más allá de este módulo, sigue este arco: el módulo 1 te da el porqué y el primer contacto; el 2, la anatomía del script k6 y los VUs (los usuarios virtuales); el 3, las métricas a fondo (latencia, percentiles, throughput, errores); el 4, los perfiles de carga (stages, rampas, executors); el 5, los thresholds (los umbrales que hacen pasar o fallar la prueba, un quality gate); el 6, checks, groups y escenarios realistas (verificar corrección bajo carga, correlacionar un flujo cotizar→reservar); el 7, analizar resultados y correr en CI; y el 8, un proyecto capstone con una prueba de carga completa de Reservo.

La regla del entorno: qué se ejecuta y qué es contenido

Esta guía tiene una regla de honestidad que conviene entender desde ya, porque explica por qué a veces verás salida "de verdad" y a veces "así se vería". k6 es un binario aparte (escrito en Go) que no está instalado en el entorno donde se preparó esta guía. Por eso, todo lo de k6 —sus scripts .js y su resumen de salida (con http_req_duration, p95, checks)— va como contenido claramente rotulado: es correcto y fiel a la documentación oficial de k6, pero no fue ejecutado aquí, y nunca te lo presentaremos como si lo hubiera sido.

En cambio, todo lo de Python sí se ejecuta y se cita: la API de Reservo local, y un mini-generador de carga que lanza peticiones concurrentes y mide latencias reales con concurrent.futures y urllib. Así, aunque el runner k6 sea contenido, los conceptos de carga —latencia, percentiles, throughput, punto de quiebre— los ves con números medidos de verdad. El generador Python es el "hermano ejecutable" del script k6: hace conceptualmente lo mismo (lanzar tráfico y medir), a menor escala, para que toques los números con las manos. Cuando en el módulo 7 veamos CI, el archivo de GitHub Actions también irá como contenido. Y en ningún momento esta guía ejecuta git ni gh.

La frontera: qué se enseña aquí y qué en las guías hermanas

Esta guía es la punta de carga de la pirámide de testing, y tiene una regla clara sobre qué le toca. Le toca performance y load testing con k6. Lo demás se enseña en las guías hermanas del ecosistema Testing, y se asume o se enlaza aquí:

TemaDónde vive
Qué es un test, assert, pytest, TDDtesting-fundamentals-and-tdd-guide (se asume)
Testing funcional E2E por el navegador (corrección de la UI)e2e-testing-with-playwright-guide (guía hermana)
Performance y carga (¿aguanta? ¿qué tan rápido bajo presión?)Esta guía
Optimizar la app/BD (índices, caché) tras encontrar el cuello de botellaFuera de alcance (se menciona como el "después")

La regla mecánica para recordarlo: si la pregunta es "¿da la respuesta correcta?" —sea una función aislada o un flujo por el navegador—, es testing funcional (otra guía). Si es "¿cuántos usuarios aguanta y qué tan rápido responde bajo presión?", es esta guía. La guía de Playwright y esta son primas cercanas —las dos prueban el sistema "de verdad", no funciones aisladas—, pero preguntan cosas distintas: Playwright maneja el navegador para verificar que el usuario ve lo correcto (corrección); k6 y el generador Python golpean la API con volumen para medir si aguanta (carga). Misma Reservo, dos preguntas.

Errores comunes

Creer que una suite funcional verde significa que el sistema "está listo". Qué pasa: todos los tests unitarios y E2E pasan, así que se declara el sistema listo para producción —y el día del lanzamiento, con tráfico real, la API responde en ocho segundos o devuelve errores 500—. Por qué pasa: se confunde correcto con listo. Una suite funcional prueba la corrección con la cocina vacía; nunca simula el viernes por la noche. Cómo detectarlo: si nunca mediste cuántas peticiones concurrentes aguanta tu sistema ni cuál es su p95 bajo carga, no sabes si está listo, por muy verde que esté tu suite funcional. Cómo corregirlo: agrega una prueba de carga a tu definición de "listo". Eso es, literalmente, para lo que existe esta guía.

Pensar que "más rápido en mi máquina" equivale a "rápido bajo carga". Qué pasa: alguien mide una petición sola, ve que tarda 3 milisegundos, y concluye que la API es veloz. Luego, bajo 200 usuarios concurrentes, esa misma API tarda 800 milisegundos porque las peticiones hacen fila. Por qué pasa: una petición aislada no compite por recursos; cientos concurrentes sí. La latencia bajo carga es una propiedad distinta de la latencia en reposo. Cómo detectarlo: si tu única medición fue con curl una vez, mediste el reposo, no la carga. Cómo corregirlo: mide con concurrencia —es exactamente lo que hace el generador Python de la lección 7, y lo verás: la misma API pasa de un p95 de 0.35 ms sin contención a decenas de milisegundos con 50 clientes a la vez—.

Confundir esta guía con la de Playwright porque "las dos prueban la app entera". Qué pasa: alguien intenta usar k6 para verificar que un botón de la página funciona, o Playwright para medir cuántos usuarios aguanta el backend. Por qué pasa: ambas herramientas ejercen el sistema "de verdad", y es fácil mezclarlas. Cómo detectarlo: si tu pregunta es "¿el usuario ve el resultado correcto en la pantalla?", estás en el terreno de Playwright (corrección de la UI); si es "¿cuántas peticiones por segundo aguanta la API y con qué p95?", estás en el terreno de k6 (carga). Cómo corregirlo: usa la regla de la frontera —corrección por el navegador → Playwright; carga contra la API → esta guía— y enlázalas, no las mezcles.

Ejercicios

Ejercicio 1 — Clasifica: ¿funcional o de carga? Para cada pregunta sobre Reservo, decide si la responde un test funcional (corrección) o una prueba de carga (rendimiento bajo presión), y explica por qué en una frase. (a) "¿POST /quote de Focus/basic/3h devuelve 7500?" (b) "¿Cuántas cotizaciones por segundo aguanta la API antes de que la latencia se dispare?" (c) "¿El descuento pro del 20% se aplica bien para 1, 2 y 3 horas?" (d) "Con 300 usuarios cotizando a la vez, ¿cuál es la latencia p95?"

Ver solución
  • (a) Funcional (corrección). Fija una entrada exacta y verifica una salida exacta; se prueba con una sola petición, sin volumen. Es el trabajo de un test funcional (unitario o E2E), no de una prueba de carga.
  • (b) Carga (rendimiento). Pregunta por la capacidad —cuántas peticiones por segundo antes de degradarse—, que solo se responde lanzando tráfico creciente y midiendo. Es una prueba de carga (tipo stress, como verás en la lección 4).
  • (c) Funcional (corrección). Son tres verificaciones de la misma lógica de cálculo, variando solo las horas. Se prueban con tres peticiones aisladas, verificando el número. Corrección pura; ni una gota de carga.
  • (d) Carga (rendimiento). Fija un nivel de concurrencia (300 usuarios) y mide una métrica de latencia (p95). Es exactamente el tipo de pregunta que responde una prueba de carga, y el p95 es la métrica central (lección 3).

La lección: si la pregunta fija una entrada y verifica una salida, es funcional; si fija un nivel de tráfico y mide tiempos o volumen, es de carga. (a) y (c) son funcionales; (b) y (d), de carga.

Ejercicio 2 — Lee el contrato de la API. Sin correr nada todavía, mira la tabla de la API canónica de arriba y responde: (a) ¿Qué método y ruta usarías para cotizar, y qué campos lleva el cuerpo de la petición? (b) Según las tarifas y el descuento pro, ¿cuánto devuelve (price_cents) POST /quote con {"room": "Studio", "tier": "pro", "hours": 2}? (c) ¿Qué tres campos trae la respuesta de POST /book?

Ver solución
  • (a) POST /quote, con un cuerpo JSON {"room": ..., "tier": ..., "hours": ...} —los tres campos: la sala, el plan y las horas—. Fíjate en que es un POST (lleva cuerpo), no un GET.
  • (b) Studio cuesta 4000 centavos/hora. Sin descuento serían 4000 × 2 = 8000. El plan pro aplica 20% de descuento entero: 8000 × 80 // 100 = 6400. La respuesta es {"price_cents": 6400}. (Todo en centavos enteros; nunca un float.)
  • (c) booking_id (el identificador de la reserva, p. ej. "bk_Focus_basic_3"), price_cents (el precio confirmado) y confirmed (true).

Ejercicio 3 — Ubica cada necesidad en su guía. Para cada situación, decide si la resuelves con esta guía (carga con k6), con la de Playwright (E2E) o con la de fundamentos, y di cuál en una frase. (a) "Quiero verificar que, al hacer clic en Cotizar en la página, el usuario ve $75.00." (b) "Quiero saber si mi API aguanta 500 reservas concurrentes sin caerse." (c) "Quiero entender qué es assert y escribir mi primer test." (d) "Quiero medir cuánto sube la latencia p95 de /quote cuando paso de 50 a 200 usuarios."

Ver solución
  • (a) Playwright (E2E). "El usuario, en la página, ve el resultado correcto" es corrección por el navegador: el terreno de e2e-testing-with-playwright-guide.
  • (b) Esta guía (carga con k6). "¿Aguanta 500 concurrentes sin caerse?" es capacidad bajo carga: exactamente lo que mide una prueba de carga (un stress test, lección 4).
  • (c) Fundamentos: testing-fundamentals-and-tdd-guide. Qué es un test, assert y pytest son el cimiento de la pirámide; ambas guías de la punta (Playwright y esta) los asumen.
  • (d) Esta guía (carga con k6). Medir cómo cambia el p95 al subir la concurrencia es el corazón del performance testing —de hecho, es lo que hace el generador Python de la lección 7, y lo verás con números reales—.

La regla mecánica: "¿da lo correcto?" por el navegador → Playwright; "¿qué es un test?" → fundamentos; "¿aguanta y qué tan rápido bajo carga?" → esta guía.

Resumen y siguiente paso

En esta lección instalaste la idea que sostiene los ocho módulos: hay dos preguntas distintas que le puedes hacer a un sistema. El testing funcional pregunta ¿da la respuesta correcta? (corrección); el testing de carga y rendimiento pregunta ¿aguanta cuando muchos usuarios pegan a la vez, y qué tan rápido responde bajo presión? (rendimiento). Son ejes independientes —correcto no implica listo, rápido no implica correcto—, y necesitas medir los dos. Esta guía es sobre el segundo, como el chef que no solo cata el risotto sino que simula el viernes por la noche.

Conociste Reservo, la API canónica sobre la que trabajamos toda la guía: un servidor mínimo con GET /rooms, POST /quote {room,tier,hours}{price_cents} y POST /book{booking_id,confirmed}, con precios en centavos enteros y descuento pro entero. Y sobre todo los números-ancla —Focus/basic/3h → 7500, Focus/pro/3h → 6000—, medidos de verdad contra el servidor en localhost. También quedó clara la regla del entorno: Python se ejecuta y se cita; k6 va como contenido rotulado, correcto pero no ejecutado.

Antes de avanzar deberías poder: enunciar con tus palabras la diferencia entre una pregunta funcional y una de carga; explicar por qué una suite funcional verde no garantiza que el sistema aguante producción; nombrar los tres endpoints de Reservo y sus dos números-ancla (7500 y 6000); y recordar la frontera con la guía de Playwright (allá corrección por el navegador, aquí carga contra la API).

Lo que sigue es profundizar en esa distinción central. En la lección 2 abrimos a fondo el contraste funcional vs carga: cómo un test funcional fija la entrada y verifica la salida, cómo un test de carga fija el tráfico y mide latencia/throughput/errores, y por qué una y otra pueden dar veredictos opuestos sobre el mismo sistema.

Recursos