Playwright: API testing nativo — request sin browser, mismo framework
Sin Postman ni RestAssured. Playwright trae request integrado para APIs. storageState + CSRF token + 4 tests contra Serenity. Proceso real.
Contexto:
En el post anterior Playwright + TypeScript: ejecución paralela experimenté con ejecución paralela. Los tests que tengo son todos de UI — abren browser, renderizan páginas, interactúan con la grilla. El más pesado tarda 11-14 segundos porque lee 91 clientes del DOM.
Esos datos vienen de una API. La grilla de Serenity llama a un endpoint REST y recibe JSON. ¿Por qué no testear ese endpoint directamente?
Playwright tiene request integrado — un APIRequestContext que hace GET, POST, PUT, DELETE sin levantar browser. Mismo expect(), mismo framework, mismo config. No es un plugin, no es una librería aparte. Viene de fábrica.
En Selenium esto no existe. Si querés testear APIs, necesitás RestAssured, HttpClient, o alguna librería separada con otra sintaxis y otro setup. En mi serie de Postman, usé Postman + Newman como herramienta dedicada. Acá la pregunta es: ¿puedo hacer lo mismo desde Playwright, sin salir del framework?
DevTools: descubriendo la API
Antes de escribir código, necesitaba saber contra qué endpoints testear. En vez de buscar documentación, hice lo que haría en un proyecto real: observar el tráfico.
Abrí Chrome, navegué a demo.serenity.is, abrí DevTools → Network → Fetch/XHR, y navegué a la grilla de Clientes.
Aparecieron 2 requests:

/Services/Northwind/Customer/List. Status 200, respuesta en JSON. Este es el endpoint que vamos a testear.El GET a /Northwind/Customer carga la página. El POST a /Services/Northwind/Customer/List es la API que trae los datos. Serenity sigue el patrón /Services/Módulo/Entidad/Acción para todos sus endpoints.
Miré el payload:

Y la respuesta:

Entities. Cada cliente con CustomerID, CompanyName, ContactName, City, Country. Los mismos datos que la grilla renderiza en UI.JSON limpio. Un array Entities con objetos de clientes. Los mismos datos que la grilla muestra en pantalla, pero sin pasar por el DOM.
Primer intento: request desnudo
Creé tests/learning/serenity-api.spec.ts con lo mínimo:
import { test, expect } from '@playwright/test';
test.describe('API testing nativo de Playwright', () => {
test('listar clientes via API', async ({ request }) => {
const response = await request.post('https://demo.serenity.is/Services/Northwind/Customer/List', {
data: {
Take: 100,
Sort: ['CustomerID']
}
});
console.log('Status:', response.status());
console.log('Body:', await response.text());
expect(response.ok()).toBeTruthy();
});
});
El fixture { request } es el APIRequestContext de Playwright. No necesita browser, no necesita page. Solo HTTP.
Resultado:
Status: 400
Body:
400 Bad Request. Body vacío. La API me rechazó sin explicación.
Era esperable: la API necesita autenticación. Sin cookies de sesión, Serenity no acepta el request.
Segundo intento: storageState sin CSRF
Ya tenía storageState configurado — el archivo .auth/user.json que guarda las cookies del login. Lo usé para crear un contexto de API autenticado:
test('listar clientes via API', async ({ playwright }) => {
const apiContext = await playwright.request.newContext({
baseURL: 'https://demo.serenity.is',
storageState: '.auth/user.json'
});
const response = await apiContext.post('/Services/Northwind/Customer/List', {
data: {
Take: 100,
Sort: ['CustomerID']
}
});
console.log('Status:', response.status());
console.log('Body:', (await response.text()).substring(0, 500));
expect(response.ok()).toBeTruthy();
await apiContext.dispose();
});
La idea: storageState le pasa las cookies de sesión al contexto de API. Playwright las manda automáticamente con cada request.
Resultado:
Status: 400
Body:
Mismo 400. Mismo body vacío. Las cookies no alcanzaron.
La conexión con Postman: el CSRF token
Este error me resultó familiar. En mi serie de Postman, tuve exactamente el mismo problema: requests que fallaban con 400 a pesar de tener cookies de sesión válidas.
La causa: ASP.NET Core Antiforgery. Serenity no solo verifica que estés autenticado — también verifica que cada POST incluya un token CSRF en el header x-csrf-token. Es una protección contra Cross-Site Request Forgery.
Miré .auth/user.json. Ahí estaban las 3 cookies:
{
"cookies": [
{
"name": ".AspNetCore.Antiforgery_...",
"value": "CfDJ8CI7vciz...",
"httpOnly": true
},
{
"name": ".AspNetAuth",
"value": "CfDJ8CI7vciz...",
"httpOnly": true,
"secure": true
},
{
"name": "CSRF-TOKEN",
"value": "CfDJ8CI7vciz...",
"httpOnly": false
}
]
}
La cookie CSRF-TOKEN tiene httpOnly: false. Está diseñada para que JavaScript la lea y la mande como header. Es lo que el frontend de Serenity hace automáticamente en cada request. Pero Playwright no lo hace solo — hay que hacerlo explícito.
La solución: leer el CSRF del storageState
import { test, expect } from '@playwright/test';
import fs from 'fs';
const storageState = JSON.parse(fs.readFileSync('.auth/user.json', 'utf-8'));
const csrfToken = storageState.cookies.find(c => c.name === 'CSRF-TOKEN')?.value || '';
test.describe('API testing nativo de Playwright', () => {
let apiContext;
test.beforeAll(async ({ playwright }) => {
apiContext = await playwright.request.newContext({
baseURL: 'https://demo.serenity.is',
storageState: '.auth/user.json',
extraHTTPHeaders: {
'x-csrf-token': csrfToken
}
});
});
test.afterAll(async () => {
await apiContext.dispose();
});
// tests...
});
Leo .auth/user.json, busco la cookie CSRF-TOKEN, y la paso como extraHTTPHeaders al crear el contexto. Cada request que salga de apiContext va a incluir las cookies de sesión + el header x-csrf-token.
Resultado:

Status 200. Los clientes llegaron en JSON. Sin browser, sin UI.
Los 4 tests
Con la autenticación resuelta, armé 4 tests que cubren el rango básico:

beforeAll, tests de listar 91 clientes y validar ALFKI.
Qué cubre cada uno
Listar clientes — status 200, estructura del response (Entities existe), cantidad exacta (91 clientes).
Primer cliente — validación de datos específicos. Take: 1 + Sort: ['CustomerID'] devuelve ALFKI. Verifico 5 campos: ID, empresa, contacto, ciudad, país.
Filtrar por Argentina — EqualityFilter: { Country: 'Argentina' } devuelve 3 clientes. Itero todos y verifico que cada uno tenga Country: 'Argentina'. Es el mismo filtro que la grilla usa cuando seleccionás un país en el dropdown.
Sin autenticación — contexto limpio, sin cookies, sin CSRF. La API devuelve 400. Este es un test negativo: verifico que la API rechaza requests no autenticados.
Resultado

5 passed. El test de ALFKI tardó 301ms. Via UI, ese mismo tipo de validación tarda 6-8 segundos porque hay que esperar render de grilla, login visual, navegación. Acá no hay DOM, no hay CSS, no hay esperas. Solo HTTP y JSON.
Los detalles en el reporte
Playwright loguea cada paso de los tests de API igual que con UI:




Lo que no hice (y por qué)
No hice CRUD completo (Create, Update, Delete, Retrieve). Ya lo hice en mi serie de Postman con 9 posts dedicados, incluyendo testing negativo, schema validation, data-driven con CSV, y CI/CD con Newman.
El objetivo acá no es replicar eso. Es mostrar que Playwright puede hacerlo desde el mismo framework que uso para tests de UI. Si necesitara un CRUD completo via API en este proyecto, usaría exactamente el mismo patrón: apiContext.post(), apiContext.put(), apiContext.delete(), con las mismas assertions.
Contraste con Selenium y con Postman
Con Selenium + Java
En Selenium no existe API testing nativo. Si quiero testear una API desde mi framework de Selenium, tengo que:
- Agregar RestAssured o HttpClient como dependencia en el
pom.xml - Aprender otra sintaxis de requests y assertions
- Manejar autenticación por separado
- Mantener dos sistemas distintos de reporting
Son dos mundos separados que conviven en el mismo proyecto pero no comparten nada.
Con Postman + Newman
Postman es una herramienta dedicada a API testing. Es excelente para explorar, debuggear y construir colecciones. Pero es un sistema separado: archivos JSON, variables de colección, pre-request scripts en JavaScript, Runner propio, Newman para CI/CD.
Con Playwright
request es parte del framework. Mismas assertions (expect), mismo sistema de reportes, mismo config, misma autenticación (storageState). No hay dos mundos — es un solo framework que testea UI y API.
No digo que Playwright reemplaza a Postman. Postman sigue siendo mejor para explorar y debuggear APIs interactivamente. Pero para tests automatizados que conviven con tests de UI, Playwright lo integra sin costuras.
Estado actual
4 tests de API pasando en tests/e2e/api-clientes.spec.ts. Sin browser, sin UI. El más rápido en 301ms. Autenticación resuelta reutilizando storageState + CSRF token extraído de las cookies.
El spec convive con login.spec.ts y clientes.spec.ts en e2e/. Mismo dominio (clientes), distinta vía (API vs UI). Corre junto con el resto de la suite.
Próximo post: network interception — cómo interceptar y modificar requests HTTP durante los tests de UI.
—
🔗 Todo el código de esta serie está en: github.com/cesarbeassuarez/playwright-typescript-framework