test.beforeEach ejecuta el setup antes de cada test en su ámbito, así que el código de login escrito una sola vez dentro de un bloque describe sirve para todos los tests dentro de él sin tocar los de afuera. test.beforeAll se ejecuta una vez para todo el grupo, no una vez por test: un token de auth que crea se comparte entre todos los tests del archivo, lo que genera interferencia cuando esos tests se ejecutan en paralelo. Este artículo cubre las reglas de ámbito que hacen útiles los bloques describe, el orden de ejecución de los hooks anidados, cuándo test.beforeAll es apropiado y cuándo es peligroso, y el comportamiento de test.only que rompe CI silenciosamente cuando se commitea.
El test básico
Antes de agregar estructura, un test individual se ve así:
import { test, expect } from '@playwright/test';
test('el usuario puede iniciar sesión', async ({ page }) => {
await page.goto('/login');
await page.fill('[data-testid="email"]', 'user@test.com');
await page.fill('[data-testid="password"]', 'ValidPass1');
await page.click('[data-testid="submit"]');
await expect(page).toHaveURL('/dashboard');
});Simple. Pero si tienes 10 tests que todos arrancan con los mismos pasos de login, eso son 10 bloques duplicados. Cuando el flujo de login cambia, actualizas 10 lugares en vez de uno.
test.beforeEach: ejecutar antes de cada test
beforeEach ejecuta código de setup antes de cada test en su ámbito. El uso más común: navegar a una página o iniciar sesión.
import { test, expect } from '@playwright/test';
test.beforeEach(async ({ page }) => {
await page.goto('/login');
await page.fill('[data-testid="email"]', 'user@test.com');
await page.fill('[data-testid="password"]', 'ValidPass1');
await page.click('[data-testid="submit"]');
await page.waitForURL('/dashboard');
});
test('el dashboard muestra el nombre del usuario', async ({ page }) => {
// Ya está logueado y en /dashboard
await expect(page.getByTestId('user-name')).toContainText('Test User');
});
test('el usuario puede acceder a la configuración', async ({ page }) => {
// También ya está logueado
await page.getByTestId('settings-link').click();
await expect(page).toHaveURL('/settings');
});Cada test arranca con el usuario ya autenticado y en el dashboard. Sin duplicación.
test.afterEach: limpiar después de cada test
afterEach se ejecuta después de cada test, independientemente de si pasó o falló. Úsalo para limpiezas que deben ocurrir después de cada test.
test.afterEach(async ({ page }, testInfo) => {
// Capturar pantalla ante un fallo (Playwright también puede hacerlo desde la config)
if (testInfo.status !== testInfo.expectedStatus) {
await page.screenshot({ path: `screenshots/${testInfo.title}.png` });
}
});O para limpieza de API:
let createdUserId: number;
test.beforeEach(async ({ request }) => {
const response = await request.post('/api/users', {
data: { email: 'temp@test.com', password: 'Pass1' },
});
const body = await response.json();
createdUserId = body.id;
});
test.afterEach(async ({ request }) => {
// Eliminar el usuario creado durante el setup
await request.delete(`/api/users/${createdUserId}`);
});test.describe: agrupar tests relacionados
test.describe crea un grupo de tests con nombre. Es útil para agrupar tests por función o página, aplicar beforeEach/afterEach solo a un subconjunto de tests, y anidar escenarios relacionados.
import { test, expect } from '@playwright/test';
test.describe('Página de login', () => {
test('muestra los campos de email y contraseña', async ({ page }) => {
await page.goto('/login');
await expect(page.getByTestId('email')).toBeVisible();
await expect(page.getByTestId('password')).toBeVisible();
});
test('muestra error con contraseña incorrecta', async ({ page }) => {
await page.goto('/login');
await page.fill('[data-testid="email"]', 'user@test.com');
await page.fill('[data-testid="password"]', 'ContraseñaErrada');
await page.click('[data-testid="submit"]');
await expect(page.getByTestId('error-message')).toBeVisible();
});
});
test.describe('Dashboard', () => {
test.beforeEach(async ({ page }) => {
// Login antes de cada test del dashboard
await page.goto('/login');
await page.fill('[data-testid="email"]', 'user@test.com');
await page.fill('[data-testid="password"]', 'ValidPass1');
await page.click('[data-testid="submit"]');
await page.waitForURL('/dashboard');
});
test('muestra el nombre del usuario', async ({ page }) => {
await expect(page.getByTestId('user-name')).toBeVisible();
});
test('muestra los pedidos recientes', async ({ page }) => {
await expect(page.getByTestId('orders-section')).toBeVisible();
});
});El beforeEach de login solo aplica al bloque describe del Dashboard. Los tests de la página de login no se ven afectados.
test.beforeAll y test.afterAll
beforeAll se ejecuta una vez antes de todos los tests en su ámbito, no antes de cada uno. afterAll se ejecuta una vez al final.
Caso de uso: setup costoso que solo necesita ocurrir una vez, como crear un usuario de test via API, sembrar una base de datos o iniciar un servidor.
import { test, expect, request as playwrightRequest } from '@playwright/test';
let authToken: string;
test.beforeAll(async () => {
// Crear token de auth una vez para todos los tests del archivo
const ctx = await playwrightRequest.newContext();
const response = await ctx.post('/api/auth/login', {
data: { email: 'admin@test.com', password: 'AdminPass1' },
});
const body = await response.json();
authToken = body.token;
await ctx.dispose();
});
test('el admin puede ver todos los usuarios', async ({ request }) => {
const response = await request.get('/api/admin/users', {
headers: { Authorization: `Bearer ${authToken}` },
});
expect(response.status()).toBe(200);
});
test('el admin puede eliminar un usuario', async ({ request }) => {
// También usa authToken — compartido entre todos los tests
});beforeAll y afterAll se ejecutan en un contexto compartido. Los cambios en el estado compartido persisten entre tests y pueden generar tests flaky si no tienes cuidado. Prefiere beforeEach para la mayoría de los setups.Bloques describe anidados
Puedes anidar bloques describe para crear una jerarquía:
test.describe('Flujo de compra', () => {
test.beforeEach(async ({ page }) => {
await loginAsUser(page);
await addItemToCart(page, 'product-123');
});
test.describe('con tarjeta válida', () => {
test.beforeEach(async ({ page }) => {
await fillShippingAddress(page);
await fillValidCard(page, '4242 4242 4242 4242');
});
test('completa la compra', async ({ page }) => {
await page.getByTestId('place-order').click();
await expect(page.getByTestId('order-confirmation')).toBeVisible();
});
test('envía email de confirmación', async ({ page }) => {
// ...
});
});
test.describe('con tarjeta inválida', () => {
test('muestra mensaje de error', async ({ page }) => {
await fillValidCard(page, '0000 0000 0000 0000');
await page.getByTestId('place-order').click();
await expect(page.getByTestId('payment-error')).toBeVisible();
});
});
});Orden de ejecución de hooks para un test anidado:
1. beforeEach externo (login + agregar al carrito)
2. beforeEach interno (completar dirección + datos de tarjeta)
3. El test en sí
4. afterEach interno (si existe)
5. afterEach externo (si existe)
test.skip y test.only
Dos modificadores útiles durante el desarrollo:
// Omitir este test (lo marca como omitido, no como fallido)
test.skip('función no implementada todavía', async ({ page }) => {
// ...
});
// Ejecutar solo este test (ignora todos los demás en el archivo)
test.only('depurando este caso específico', async ({ page }) => {
// ...
});test.only a la rama principal. Hace que toda la suite de CI falle al ejecutar solo un test.Skip condicional, útil para tests específicos por entorno:
test('panel de administración', async ({ page }) => {
test.skip(process.env.ENV === 'production', 'Omitido en producción');
// ...
});Cómo se nombran los tests en los reportes
El nombre del test que aparece en los reportes combina la etiqueta del describe con la del test:
test.describe('Página de login', () => {
test('muestra error con contraseña incorrecta', async ({ page }) => { /* ... */ });
});
// En el reporte aparece como: "Página de login > muestra error con contraseña incorrecta"Escribe nombres descriptivos. Cuando un pipeline de CI muestra 3 fallos y necesitas entenderlos sin abrir el código, los vas a agradecer.
Ejemplo de estructura completa de archivo
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
test.describe('Autenticación de usuario', () => {
let loginPage: LoginPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
await loginPage.navigate();
});
test.describe('Credenciales válidas', () => {
test('redirige al dashboard', async ({ page }) => {
await loginPage.login('user@test.com', 'ValidPass1');
await expect(page).toHaveURL('/dashboard');
});
test('establece la cookie de auth', async ({ page }) => {
await loginPage.login('user@test.com', 'ValidPass1');
const cookies = await page.context().cookies();
expect(cookies.some(c => c.name === 'auth_token')).toBe(true);
});
});
test.describe('Credenciales inválidas', () => {
test('contraseña incorrecta muestra error', async () => {
await loginPage.login('user@test.com', 'ContraseñaErrada');
await expect(loginPage.errorMessage).toBeVisible();
});
test('email vacío muestra error de validación', async () => {
await loginPage.login('', 'ValidPass1');
await expect(loginPage.emailError).toContainText('requerido');
});
});
});Referencia de hooks
| Hook | Cuándo se ejecuta | Usar para |
|------|-------------------|-----------|
| test.beforeEach | Antes de cada test en el ámbito | Navegación, login, resetear estado |
| test.afterEach | Después de cada test en el ámbito | Limpieza, capturas ante fallos |
| test.beforeAll | Una vez antes de todos los tests | Setup costoso de una sola vez |
| test.afterAll | Una vez después de todos los tests | Teardown de una sola vez |
| test.describe | (agrupación, no es un hook) | Organizar tests, acotar hooks |
Empieza con test.beforeEach para la mayoría de los setups. Agrega test.describe para agrupar tests relacionados. Usa test.beforeAll solo cuando el setup sea genuinamente costoso y seguro de compartir entre tests.