Probar el backend directo — sin navegador, sin clics.
La fixture request · aserciones de respuesta · validación de schema · KATA para API.
Sesión de API automation con Playwright. Prerequisito: ya entienden tests, clases y fixtures básicas (sesión Automation Patterns). Hoy bajamos del navegador a la capa HTTP: probar endpoints directo. Misma promesa pedagógica: primero el dolor, luego la herramienta. Todo en vivo — un runner que ejecuta JavaScript real contra una API falsa en memoria, un trazador del ciclo de vida de una request, un simulador que compara velocidad API vs UI, y un trazador de capas KATA. Que la sala TOQUE cada widget.
Escribir un test() · Page Objects · fixtures con use(). Probar la app por la pantalla.
Probar la app por debajo de la pantalla: golpear el endpoint HTTP directo. Más rápido, más estable, más barato.
// La pirámide del testing /\ E2E (UI) lento, frágil / \ pocos /----\ API ⟵ hoy: rápido / \ muchos y estable /--------\ Unit los devs
La capa API es el punto dulce de QA: cubre lógica de negocio real, sin el costo ni la fragilidad de manejar un navegador.
Encuadre: la pirámide de testing. Arriba E2E (UI) — realista pero lento y frágil. Abajo unit (de los devs). En medio, la capa API: el punto dulce de QA. Un test de API arranca en milisegundos (no abre navegador), no se rompe porque cambió un color o un botón, y prueba la lógica de negocio de verdad: validaciones, permisos, estados. Mensaje clave de la sesión: si un caso se puede probar por API, pruébalo por API — guarda el navegador para lo que SOLO se ve en pantalla.
GET, POST, PUT, DELETE sobre una URL, y comprueba lo que vuelve. Sin pantalla, sin clics — solo petición y respuesta. Hoy aprendemos a escribirlos con Playwright.Calentamiento que fija el vocabulario: petición → respuesta. La opción A confunde API con E2E (el error más común al empezar). La C lo confunde con análisis estático. Si la sala duda, recálcales: API test = mandar una request HTTP y verificar la response. Es exactamente lo que hace Postman, pero automatizado y dentro de la suite.
test("el pedido se crea", async ({ page }) => { await page.goto("/login"); ┐ await page.fill("#email", EMAIL); │ 8 segundos de await page.fill("#password", PASS); │ navegador, clics await page.click("#login"); │ y esperas… await page.goto("/orders/new"); │ await page.fill("#item", "Coffee"); │ await page.click("#submit"); ┘ await expect(page.locator(".toast")).toBeVisible(); // ¿de verdad se guardó? });
Solo quería saber si POST /orders funciona. Pagué 8 segundos, un login completo y un navegador… y al final confío en un toast, no en la respuesta real del servidor.
Deja que ELLOS sientan el desperdicio: para verificar UN endpoint, montamos login + navegación + formulario + un navegador entero. Y peor: la aserción final mira un toast en pantalla, no la respuesta HTTP. Si el toast aparece pero el backend devolvió 500, el test pasa en falso. El gancho del acto: hay una puerta directa al backend que evita TODO esto. Sostén la incomodidad antes de revelar la fixture request.
request — HTTP sin navegador 🚀test("POST /orders", async ({ request }) => { // ▲ // otra fixture, como page const res = await request.post("/orders", { data: { item: "Coffee", qty: 2 }, }); await expect(res).toBeOK(); // 2xx });
Sin goto, sin clics, sin esperar render. Una petición directa, una respuesta directa. Milisegundos, no segundos.
request es una fixture built-inIgual que page, Playwright te la presta lista. Es un cliente HTTP: APIRequestContext.
.get() · .post() · .put() · .delete(). El mismo idioma del backend.
Pides { request } en vez de { page }. Mismo await, mismo expect. Solo cambia el canal.
La revelación del acto: existe una fixture hermana de page llamada request. Es un APIRequestContext — un cliente HTTP completo que Playwright arma y limpia por ti, con el baseURL y los headers de tu config ya aplicados. Mensaje tranquilizador: no es una librería nueva ni conceptos nuevos; pides { request } en lugar de { page } y ya. El cambio mental es: dejas de manejar la pantalla y empiezas a hablar con el servidor en su idioma (verbos HTTP). El toBeOK lo vemos a fondo en el acto 2.
// leer await request.get("/orders"); await request.get("/orders/42"); // crear (con cuerpo JSON) await request.post("/orders", { data }); // actualizar / borrar await request.put("/orders/42", { data }); await request.delete("/orders/42");
const res = await request.get("/orders/42"); res.status(); // 200, 404, 500… res.ok(); // true si es 2xx await res.json(); // el body como objeto JS res.headers(); // los headers
status() y ok() son síncronos. json() es async — el body llega como stream, por eso lleva await.
El mapa mínimo y verídico de Playwright. Izquierda: los 4 verbos. El segundo argumento { data } serializa a JSON automáticamente y pone el header content-type. Derecha: la respuesta (APIResponse). Detalle que confunde a todos: status() y ok() son síncronos (ya tienes el status apenas vuelve), pero json() es async porque el cuerpo se lee como stream — de ahí el await res.json(). En la siguiente slide lo ejecutan en vivo contra una API falsa que responde de verdad.
tu test envía peticiones HTTP →
Este runner EJECUTA JavaScript real: la clase OrdersApi corre tal cual contra una API falsa en memoria que valida, escribe en una "base de datos" visible y devuelve status reales. Tres demos en vivo: (1) ejecutar tal cual → POST 201, GET 200, PASS verde, y la fila aparece en la DB; (2) cambiar expect(body.item).toBe("Coffee") por "Tea" → FAIL con expected/received legible; (3) el experimento estrella: dentro de createOrder, cambiar "/orders" por "/order" (typo) → POST /order → 404 → falla → corregir EN UN SOLO LUGAR → verde. Ese momento ES el argumento del componente de API: el endpoint vive centralizado, igual que un selector en un Page Object.
GET /orders sin tocar la interfaz. ¿Qué fixture pides en tu test?{ request } es el cliente HTTP: golpea el endpoint sin abrir navegador. Pedir { page } o { browser } arranca un navegador que no necesitas — segundos perdidos en cada test. Para probar la API, habla con la API.Si hicieron el experimento del endpoint en el runner, este quiz formaliza lo vivido. Opción A es el reflejo de quien aún piensa en UI; B es la respuesta. Transición: "request resuelve el canal… pero toBeOK() solo mira el status. ¿Y si el status es 200 pero el body viene mal? Ese es el siguiente dolor: aserciones de verdad."
test("trae el pedido", async ({ request }) => { const res = await request.get("/orders/42"); await expect(res).toBeOK(); // ✅ 200… ¿y ya? }); // El server respondió 200, pero el body era: // { "id": 42, "item": null, "qty": -3 } // ☠️ datos basura, y el test pasó feliz
Verificar solo el status es un colador: el endpoint puede responder 200 con un body incompleto, campos null o tipos equivocados.
1. Status — ¿el código correcto? 2. Body — ¿los datos correctos? 3. Contrato — ¿la forma correcta?
Tests "verdes" que no atrapan nada. Pasan aunque el backend devuelva basura, porque solo miran el status.
Afirmar sobre el contenido: campos exactos, tipos, y la forma completa del JSON (schema).
El dolor del acto: una aserción débil da falsa seguridad. 200 OK solo dice "el servidor respondió", no "respondió bien". Tres niveles a cubrir: status (¿201 o 422?), body (¿item es "Coffee" y qty es 2?), y contrato/schema (¿el JSON tiene todos los campos con los tipos correctos?). El ejemplo del body con null y qty negativo es el clásico: pasa el status, pasa el test, y el bug llega a producción. La cura viene en las dos slides siguientes.
test("GET /orders/42 completo", async ({ request }) => { const res = await request.get("/orders/42"); // 1 · STATUS expect(res.status()).toBe(200); // 2 · BODY (el contenido importa) const body = await res.json(); expect(body.id).toBe(42); expect(body.item).toBe("Coffee"); expect(body.qty).toBeGreaterThan(0); // 3 · HEADERS expect(res.headers()["content-type"]) .toContain("application/json"); });
toBe(201) para creación, 422 para validación, 404 para no-existe. El código cuenta una historia.
toBe · toEqual · toBeGreaterThan · toBeDefined. Las mismas aserciones que ya usas.
content-type, paginación, rate-limit, auth. No siempre, pero cuando el contrato lo exige.
La cura por capas, con matchers que YA conocen (toBe, toEqual, toBeGreaterThan) — no hay API nueva de aserciones, es el mismo expect de siempre aplicado al JSON. Mensaje sobre el status: cada código cuenta una historia y debes afirmar el EXACTO, no solo "2xx" — 201 vs 200 distingue "creé" de "ya existía", 422 vs 400 distingue validación de error genérico. El body es donde se atrapan los bugs reales. Headers: solo cuando el contrato lo pide. Pero afirmar 8 campos a mano es tedioso y frágil — eso abre la siguiente slide: validar la forma completa con schema.
En vez de afirmar 10 campos a mano, defines la forma esperada una vez (con Zod) y validas todo el body de golpe. Si falta un campo o cambia un tipo, el schema lo caza.
import { z } from "zod"; const OrderSchema = z.object({ id: z.number(), item: z.string(), qty: z.number().positive(), }); const body = await res.json(); OrderSchema.parse(body); // 💥 lanza si la forma no calza
En este repo los schemas se generan desde la OpenAPI (bun run api:sync → api/schemas/). El contrato del backend y tus tests no pueden divergir.
El salto de "afirmar campos sueltos" a "validar el contrato completo". Zod define la forma una vez y parse() lanza si algo no calza — un campo faltante, un tipo cambiado, un null inesperado. Dato verídico y potente del repo: los schemas NO se escriben a mano, se generan desde la OpenAPI del backend con bun run api:sync hacia api/schemas/. Eso significa que si el backend cambia su contrato, tus tipos se actualizan y los tests que asumían lo viejo fallan en compilación — trazabilidad real entre contrato y prueba. Demo del live cell: ejecutar tal cual → OK; cambiar qty a -3 o item a 99 → ver los errores concretos. Que la sala rompa el body y vea el validador atraparlo.
— en reposo —
Vuelve tangible el viaje de una request HTTP. Paso 1: se arma la petición (verbo POST + ruta + body JSON). Paso 2: await la envía por la red y el control se PAUSA esperando. Paso 3: el servidor la recibe, valida, escribe en DB y prepara la respuesta 201. Paso 4: la respuesta vuelve y await se resuelve con el objeto res. Paso 5: afirmamos el status (síncrono, ya está). Paso 6: await res.json() lee el body (stream) y afirmamos un campo. Mensaje a martillar: el await del paso 2 es la espera de RED; el await del paso 6 es la espera de leer el CUERPO. Dos awaits, dos esperas distintas — eso confunde a casi todos al inicio.
expect(res).toBeOK() y pasa. Pero el body llegó { item: null, qty: -3 }. ¿Qué falló en tu test?toBeOK() solo mira el status. Un 200 con datos basura pasa igual. Una buena aserción de API cubre las tres capas: status exacto, body (campos + tipos, idealmente con schema) y headers cuando el contrato lo pide. Ninguna herramienta adivina qué body es "correcto" — eso lo defines tú.Caza la falsa seguridad del status-only. Opción A es el error mental central del acto; C es el "que la herramienta piense por mí". La respuesta B reactiva las tres capas. Si aciertan masivo, internalizaron que probar API = verificar contenido, no solo conexión. Transición: "Ya sabes enviar y verificar. Pero cada test necesita un usuario logueado y datos previos. Volver a hacerlo por UN endpoint a mano es el siguiente dolor: el setup de datos."
test("actualiza un pedido", async ({ request }) => { // 1 · login para sacar token… const auth = await request.post("/login", {data:creds}); const { token } = await auth.json(); // 2 · crear un pedido para tener qué editar… const made = await request.post("/orders", { headers: { Authorization: `Bearer ${token}` }, data: { item: "Coffee", qty: 1 }, }); // 3 · …y recién AQUÍ empieza el test real 😩 });
Login + crear precondiciones antes de cada test. 15 líneas de ritual por 3 líneas de prueba real.
El Authorization: Bearer repetido en cada llamada. Cópialo mal una vez y son 401 misteriosos.
Una fixture que loguee una vez, entregue un cliente ya autenticado y datos frescos — y limpie al final. Lo que viste con use(), ahora para API.
El dolor conecta directo con la sesión de fixtures: el setup de API también se repite. Dos caras: (1) la autenticación — sacar el token y arrastrarlo en cada header; (2) las precondiciones — crear el pedido que vas a editar/borrar. La idea es la misma fixture con use() del patrón anterior, ahora preparando un request ya autenticado + datos. Subraya el riesgo del token a mano: un Bearer mal copiado da 401 que parecen bugs del backend pero son del test. La solución: centralizar auth y data en fixtures componibles — exactamente lo que KATA hace en el acto 4.
export const test = base.extend({ api: async ({ playwright }, use) => { // login UNA vez, reusa el token const ctx = await playwright.request.newContext({ baseURL: BASE, extraHTTPHeaders: { Authorization: `Bearer ${TOKEN}` }, }); await use(ctx); // el test lo recibe ya logueado await ctx.dispose(); }, });
Todas las llamadas salen ya con el token. Cero Bearer regados.
import { faker } from "@faker-js/faker"; // cada test genera SU propio dato único const order = { item: faker.commerce.productName(), qty: faker.number.int({ min: 1, max: 9 }), }; await api.post("/orders", { data: order });
Datos únicos = ningún test pisa a otro. Es el requisito para poder correr en paralelo sin choques.
Las dos piezas que ordenan el setup de API. Izquierda: una fixture api que crea un APIRequestContext con baseURL y el header Authorization ya puestos (newContext + extraHTTPHeaders) — el token se resuelve una vez y todas las llamadas salen autenticadas. dispose() limpia al final. Derecha: faker para datos únicos por test. Esto es CRÍTICO y conecta con paralelización: si dos tests crean "Coffee" con el mismo id fijo, chocan; con faker cada uno tiene su propio dato y son verdaderamente independientes. La independencia es el boleto de entrada al paralelismo, que medimos en la siguiente slide.
El widget estrella del acto. Los MISMOS 6 casos de negocio, ejecutados en tres capas. Demo: arranca en "API puro" → reloj ~5s. Cambia a "UI · E2E" → ~29s. Cambia a "Híbrido" → ~11.5s. Dos verdades que la sala debe ver: (1) la COBERTURA es idéntica — 6/6 casos en las tres; lo que cambia brutalmente es el reloj. (2) API es ~5-6× más rápido que UI para la misma verificación, porque no hay navegador, render ni esperas visuales. El híbrido (setup por API + una verificación por UI) es el punto medio realista. Mensaje: elige la capa más barata que aún pruebe lo que necesitas. Reserva la UI para lo que SOLO se ve en pantalla.
faker en vez de usar un id fijo como "order-1"?"order-1" y uno rompe al otro. Datos únicos por test = aislamiento total, y el aislamiento es lo que permite paralelizar sin flakiness. La velocidad de API solo se aprovecha si los tests no se estorban entre sí.Une datos (faker) con velocidad (paralelismo). La respuesta exige entender que la independencia es la condición para correr rápido. Opción A subestima (es un beneficio menor); C inventa una obligación. Si en el simulador vieron el salto de velocidad, aquí entienden el requisito que lo habilita: sin independencia, paralelizar multiplica flakiness, no velocidad. Transición al cierre: "request, aserciones, datos, velocidad… ¿cómo se organiza todo esto a escala en el repo real? KATA, ahora por el lado de la API."
KATA (Component Action Test Architecture) no inventa nada: organiza el cliente, las aserciones y los datos en 4 capas, donde cada una extends la anterior. El lado API es idéntico al UI — solo cambia la base.
Tu OrdersApi es un componente de dominio. Ahí viven las ATCs de API.
Status y schema fijos van dentro; las de negocio, en el test.
{ api } entrega el cliente autenticado y listo.
// La cadena de herencia REAL del repo (API) class TestContext { ... } // L1 · config, faker class ApiBase extends TestContext // L2 · helpers HTTP class OrdersApi extends ApiBase // L3 ⟵ tu cliente // y la fixture lo inyecta: class ApiFixture { orders = new OrdersApi(options); }
Misma arquitectura que el lado UI: solo cambia ApiBase por UiBase. Aprendes una vez, aplicas a ambos.
Tesis del acto: KATA es la síntesis de request + aserciones + datos en una estructura. Mapea explícito: tu cliente OrdersApi = componente (L3), las aserciones fijas viven dentro de la ATC, la auth y los datos los inyecta la fixture { api } (L4). La cadena TestContext → ApiBase → OrdersApi es real y verídica (kata-architecture.md), y es ESPEJO de la cadena UI (TestContext → UiBase → LoginPage). Mensaje que baja el miedo: el lado API y el lado UI comparten L1 (TestContext) y solo difieren en L2 (ApiBase vs UiBase). Si entendiste un lado, entiendes el otro.
Regla única: una capa superior puede usar una inferior, nunca al revés. Tu cliente de API (L3, estrella) es el corazón — escribes ATCs ahí y todo lo demás ya viene hecho.
El mapa de KATA en su versión API, verídico (kata-architecture.md §1). De abajo arriba: TestContext (utilidades globales, compartido con UI), ApiBase (helpers HTTP: envuelve request, parsea respuestas, aplica auth), Componentes/ATCs (la estrella, tu OrdersApi), Steps (cadenas reutilizables para precondiciones, opcional), Fixtures (DI, entregan el cliente listo). La regla de direccionalidad mantiene el orden. Marca la capa estrella: ahí pasa el equipo el 90% del tiempo escribiendo ATCs. ApiBase y las fixtures ya vienen en el boilerplate — no se reescriben.
class OrdersApi extends ApiBase { @atc("PROJ-201") async createOrder(data) { const res = await this.post("/orders", { data }); // aserciones FIJAS dentro: expect(res.status()).toBe(201); const body = await res.json(); OrderSchema.parse(body); // contrato return body; } }
Acceptance Test Case: no una llamada suelta, sino un mini-flujo — request → status fijo → schema → devuelve datos.
@atc("PROJ-201")El decorador ata la ATC a su ticket de Jira 1:1. Trazabilidad y tracing automático.
Status 201 y schema (siempre ciertos) van dentro. Las aserciones de negocio van en el test.
La única pieza realmente nueva: la ATC de API y el decorador @atc. Código real, espejo del LoginPage del repo. Define ATC: caso completo (request + aserciones fijas + devuelve datos para que el test siga), no una llamada aislada. Las aserciones FIJAS (status 201 esperado, schema del contrato) viven dentro porque son parte de "la acción terminó bien"; las de NEGOCIO (¿el total es el correcto? ¿aparece en la lista del usuario?) van en el test. Es la misma regla acciones-vs-decisión del lado UI. Reglas duras: ATC atómica (nunca llama a otra ATC; las cadenas son Steps), y el @atc da trazabilidad 1:1 con Jira.
— en reposo —
Cierra el círculo: una sola línea de test (api.orders.createOrder) atraviesa las 4 capas. Paso 1: el test pide la fixture { api } (L4). Paso 2: la fixture ya tiene OrdersApi instanciada y autenticada como api.orders (L4→L3). Paso 3: createOrder es la ATC en OrdersApi (L3). Paso 4: usa el helper this.post() heredado de ApiBase (L2). Paso 5: que a su vez hereda config/faker/request de TestContext (L1). Mensaje: el test escribe UNA línea legible; las 4 capas hacen el trabajo pesado debajo. La arquitectura pagando dividendos.
| Tipo de test | Fixture | ¿Navegador? | Cuándo |
|---|---|---|---|
| API pura (integration) | { api } | No | Testing de API. Default en tests/integration/ |
| Solo UI | { ui } | Sí | Flujo de UI sin setup por API |
| Híbrido | { test } | Sí | Setup por API + acción/verificación UI |
| Precondiciones repetidas | { steps } | Depende | 3+ ATCs repetidas en 3+ archivos |
// API pura — NO abre navegador (lazy) test("PROJ-7: lista pedidos", async ({ api }) => { await api.orders.getOrders({ limit: 10 }); });
// Híbrido — crea por API, verifica por UI test("PROJ-9: aparece en lista", async ({ test }) => { const o = await test.api.orders.createOrder(data); await test.ui.orders.verifyVisible(o.id); });
Cierre operativo y verídico (kata-architecture.md §7): la tabla de selección de fixture es decisión diaria. Para API pura → { api }: como las fixtures son lazy, NO abre navegador → suites de integración en milisegundos. El { test } híbrido es la joya: crea el estado por API (rápido) y verifica por UI (realista), compartiendo contexto — el patrón que el simulador de la slide 16 llamó "híbrido". Estos snippets son el patrón real del repo. Con esto, el alumno sabe LEER cualquier test y ELEGIR su fixture con criterio de costo.
createOrder(), ¿qué aserción va dentro de la ATC y cuál en el test?Integra la regla acciones-vs-decisión (acto 2 de la sesión de patrones) con la ATC de API. La respuesta A separa bien: fijas dentro (201, schema), negocio fuera (montos, permisos). B mete todo dentro (oculta el veredicto); C deja la ATC sin garantía de éxito. Si aciertan masivo, dominan la frontera más sutil de KATA. Cierre del acto: pasamos al integrador y al score.
El quiz que amarra el arco: dolor→cura, ahora como evaluación. Respuesta correcta = el alumno internalizó que cada técnica responde a un dolor concreto, no es ceremonia. KATA cierra como el meta-marco que las organiza. Tras este quiz, celebra y pasa al score final.
tests/
components/
TestContext.ts // L1
api/ApiBase.ts // L2 · helpers HTTP
api/OrdersApi.ts // L3 · cliente + ATCs
steps/*.ts // L3.5 · Steps
ApiFixture.ts // L4 · Fixtures
TestFixture.ts // L4 · híbrido
integration/** // tests API puros
e2e/** // tests UI (+API)
api/schemas/ // Zod desde OpenAPI
playwright.config.ts // baseURL, workers
El 90% del tiempo: escribir ATCs en components/api/ y orquestarlas en integration/.
TestContext, ApiBase y las Fixtures vienen en el boilerplate. Heredas helpers; no los reescribes. Los schemas se generan: bun run api:sync.
Skill /test-automation + references/kata-architecture.md: reglas de ATC, fixtures y schemas.
Aterriza todo en la estructura REAL del repo. Mensaje tranquilizador: las capas bajas (TestContext, ApiBase, fixtures) ya están construidas; el alumno trabaja sobre todo en components/api (ATCs) y integration (tests). Punteros reales: la skill /test-automation y references/kata-architecture.md tienen las reglas completas de ATC y la tabla de fixtures; los schemas Zod se generan desde la OpenAPI con bun run api:sync (no se escriben a mano). Esta slide es el puente del aprendizaje a la práctica del lunes.
| Herramienta | Dolor que cura | Pieza clave | En KATA |
|---|---|---|---|
| 🚀 request fixture | navegador para un endpoint | request.post(...) | ApiBase (L2) |
| 🎯 Aserciones + schema | "200" no significa correcto | status + body + Zod | dentro de la ATC (L3) |
| 🔧 Fixtures + faker | setup/auth/datos repetidos | { api } + datos únicos | Fixtures (L4) |
| 🥋 KATA | todo junto, a escala | 4 capas + ATC | el marco completo |
Cruzaste de "probar por la pantalla" a probar el backend directo: más rápido, más estable, más profundo. Lo que hace un SDET de API.
El recap de una mirada: tabla que cruza herramienta × dolor × pieza × lugar en KATA. Pídeles que la fotografíen o la peguen en Confluence — es el resumen portátil. Nombra el logro: pasaron de probar por la UI a probar el backend directo, que es el salto a SDET de API. Prepara el score final.
Este archivo corre offline: vuelve al runner de API, al validador de schema y al simulador de velocidad. Toca todo.
Abre tests/components/api/ del repo y ubica: capa, ATC, decorador @atc, aserciones fijas, schema.
Escribe tu primera ATC de API real con /test-automation — Plan → Code → Review sobre KATA.
Hoy: request → aserciones + schema → fixtures + faker → KATA. De la pantalla al backend.
Celebra el score y nombra el logro: hoy aprendieron a probar el backend directo — request, aserciones de contenido + schema, fixtures con auth y datos, y KATA para escalar. Todo vivido en vivo, no memorizado. Reto puente sin computadora: abrir un componente api real e identificar cada concepto en código de producción. Próximo paso accionable: la skill /test-automation para escribir su primera ATC de API. Comparte el HTML — funciona offline.
request · aserciones · schema · KATA.
Probar la API: rápido, estable y al hueso.
Slide de cierre y espacio para preguntas. Reabre el runner de API o el simulador de velocidad si alguien quiere ver algo otra vez — son la mejor herramienta para resolver dudas en vivo. Punteros finales: la skill /test-automation y la referencia de arquitectura KATA. Gracias y a escribir ATCs de API.