Playwright ha madurado hasta convertirse en la opción predeterminada para testing end-to-end en aplicaciones web modernas, y con razón: auto-waiting, soporte multi-navegador, intercepción nativa de red y un ejecutor de pruebas que entiende el paralelismo de forma nativa. Pero tener una herramienta poderosa no produce automáticamente pruebas mantenibles. Después de ejecutar Playwright a escala en múltiples equipos, y de enseñarlo en mis cursos de automatización en UPC, he compilado los patrones que separan las suites de pruebas que escalan de las que colapsan bajo su propio peso.
Estructura de Proyecto que Escala
La primera decisión que determina la mantenibilidad a largo plazo es cómo organizas tus archivos. Recomiendo esta estructura para cualquier equipo que trabaje con más de 20 archivos de prueba:
tests/
e2e/
auth/
login.spec.ts
signup.spec.ts
payments/
checkout.spec.ts
refund.spec.ts
pages/
LoginPage.ts
CheckoutPage.ts
BasePage.ts
fixtures/
auth.fixture.ts
data.fixture.ts
utils/
api-helpers.ts
test-data-factory.ts
playwright.config.ts Los principios clave: las especificaciones de prueba viven bajo tests/e2e/ organizadas por dominio funcional, los Page Objects viven bajo pages/, los fixtures personalizados bajo fixtures/ y las utilidades compartidas bajo utils/. Esta separación hace inmediatamente claro dónde pertenece el código nuevo y previene la entropía de "todo en una carpeta" que afecta a las suites de pruebas en crecimiento.
Page Object Model Bien Implementado
El Page Object Model (POM) es el patrón más recomendado para la organización de pruebas E2E, pero también es el más comúnmente mal implementado. El error típico es crear "objetos dios": una sola clase DashboardPage con 50 métodos que cubre cada interacción posible en el dashboard. Estos se vuelven imposibles de mantener porque cada cambio en el dashboard toca el mismo archivo.
En su lugar, diseña Page Objects alrededor de intenciones del usuario, no URLs de página. Un flujo de checkout podría involucrar un CartPage, un ShippingFormPage y un PaymentPage, incluso si todos se renderizan dentro de la misma ruta de aplicación de página única.
// pages/CheckoutPage.ts
import { type Page, type Locator } from '@playwright/test';
export class CheckoutPage {
readonly page: Page;
readonly shippingAddress: Locator;
readonly paymentMethod: Locator;
readonly placeOrderButton: Locator;
readonly orderConfirmation: Locator;
constructor(page: Page) {
this.page = page;
this.shippingAddress = page.getByLabel('Shipping address');
this.paymentMethod = page.getByRole('combobox', { name: 'Payment method' });
this.placeOrderButton = page.getByRole('button', { name: 'Place order' });
this.orderConfirmation = page.getByTestId('order-confirmation');
}
async fillShipping(address: string) {
await this.shippingAddress.fill(address);
}
async selectPayment(method: string) {
await this.paymentMethod.selectOption(method);
}
async placeOrder() {
await this.placeOrderButton.click();
await this.orderConfirmation.waitFor({ state: 'visible' });
}
} Observa que los localizadores se definen en el constructor usando los selectores semánticos de Playwright (getByLabel, getByRole, getByTestId), no selectores CSS crudos. Esto hace que el Page Object sea resiliente a cambios en la estructura HTML mientras permanece legible.
Patrones de Fixtures con test.extend
test.extend de Playwright es una de sus características más poderosas, pero muchos equipos la subutilizan. Los fixtures personalizados te permiten encapsular lógica de setup y teardown (sesiones autenticadas, creación de datos de prueba, estado de API) para que las especificaciones de prueba se mantengan enfocadas en la verificación de comportamiento.
// fixtures/auth.fixture.ts
import { test as base, expect } from '@playwright/test';
import { CheckoutPage } from '../pages/CheckoutPage';
type AuthFixtures = {
authenticatedPage: CheckoutPage;
};
export const test = base.extend<AuthFixtures>({
authenticatedPage: async ({ page }, use) => {
// Setup: authenticate via API to skip UI login
const response = await page.request.post('/api/auth/login', {
data: { email: 'test@example.com', password: 'secure-password' }
});
const { token } = await response.json();
await page.context().addCookies([{
name: 'session',
value: token,
domain: 'localhost',
path: '/'
}]);
await page.goto('/checkout');
const checkoutPage = new CheckoutPage(page);
await use(checkoutPage);
// Teardown: clean up test data
await page.request.delete('/api/test/cleanup');
}
});
export { expect }; Ahora cada prueba que necesita una página de checkout autenticada simplemente la declara como parámetro de fixture, sin lógica de login repetida, sin estado compartido entre pruebas y con limpieza automática en teardown.
Selectores Confiables: La Base
Las pruebas inestables frecuentemente se remontan a selectores frágiles. Mi prioridad de selectores, que aplico a través de revisión de código y reglas de linting, es:
getByRole: refleja semántica de accesibilidad. Si tu botón no es encontrable por rol, también tiene un problema de accesibilidad.getByLabel/getByPlaceholder/getByText: selectores de texto visible para el usuario. Resilientes a cambios estructurales.getByTestId: atributosdata-testiddedicados. Usar cuando los selectores semánticos son ambiguos.- Selectores CSS: último recurso. Evita nombres de clases generados por herramientas CSS-in-JS.
Prohíbo explícitamente XPath en revisiones de código. Es frágil, difícil de leer y casi siempre reemplazable con una de las estrategias anteriores.
Manejo de Pruebas Inestables
La inestabilidad es el asesino número uno de la credibilidad de una suite de pruebas. Si los desarrolladores no pueden confiar en los resultados, dejan de mirarlos. Estos son los patrones que uso para eliminar y gestionar la inestabilidad:
Auto-reintento con traza en fallo. El mecanismo de reintento integrado de Playwright combinado con la grabación de trazas te da un paquete diagnóstico completo cuando una prueba falla intermitentemente:
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
retries: process.env.CI ? 2 : 0,
use: {
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure'
},
reporter: [
['html', { open: 'never' }],
['junit', { outputFile: 'results/junit.xml' }]
]
}); Mocking de red para dependencias externas. Las pruebas que dependen de APIs de terceros (pasarelas de pago, servicios de geolocalización, proveedores de email) deben mockear esos límites. page.route() de Playwright lo hace directo:
await page.route('**/api/payment/process', route => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
transactionId: 'mock-txn-001',
status: 'approved'
})
});
}); Aislar datos de prueba. Las pruebas nunca deben compartir estado de base de datos. Cada prueba crea sus propios datos a través de fixtures de API y limpia en teardown. Si dos pruebas dependen de la misma cuenta de usuario, eventualmente colisionarán al ejecutarse en paralelo.
Ejecución Paralela y Sharding
Playwright ejecuta archivos de prueba en paralelo por defecto, que es el comportamiento correcto para CI. Sin embargo, necesitas diseñar para ello:
- Sin estado compartido entre archivos spec. Cada archivo
.spec.tsse ejecuta en su propio worker. Si dos archivos necesitan el mismo setup, usa fixtures, no estado global. - Usa sharding para suites grandes. Cuando tu suite excede 15 minutos en una sola máquina, divide entre múltiples runners de CI. Playwright soporta esto nativamente:
# .github/workflows/e2e.yml
name: E2E Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
shard: [1/4, 2/4, 3/4, 4/4]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npx playwright install --with-deps chromium
- run: npx playwright test --shard=${{ matrix.shard }}
- uses: actions/upload-artifact@v4
if: failure()
with:
name: traces-${{ matrix.shard }}
path: test-results/ Esta configuración divide la suite de pruebas entre cuatro runners paralelos. En caso de fallo, las trazas se suben como artefactos para depuración. La suite completa que toma 20 minutos en una máquina se completa en aproximadamente 5 minutos con cuatro shards.
Principios de Integración con CI
Más allá del sharding, estas prácticas de CI han demostrado ser esenciales en mi experiencia:
- Instala solo los navegadores que necesitas.
npx playwright install chromiumes más rápido que instalar los tres navegadores. Ejecuta pruebas cross-browser nocturnamente, no en cada PR. - Cachea los navegadores de Playwright. Los binarios de navegadores son grandes. Cachéalos entre ejecuciones de CI para evitar descargarlos en cada build.
- Falla rápido, depura después. Configura CI para detenerse en la primera falla durante verificaciones de PR (retroalimentación rápida), pero ejecuta la suite completa nocturnamente (cobertura completa). Dos configuraciones diferentes para dos propósitos diferentes.
- Reporta al PR. Usa el reporter HTML de Playwright y publica resultados como artefactos de CI o comentarios en el PR. Si los desarrolladores tienen que buscar entre logs de CI para encontrar fallas, la adopción sufre.
Patrones que He Aprendido a Evitar
Después de mantener suites de Playwright con cientos de pruebas, estos son los anti-patrones que detecto en revisión de código:
Esperas hardcodeadas. await page.waitForTimeout(3000) es casi siempre incorrecto. El auto-waiting de Playwright maneja la gran mayoría de problemas de timing. Si necesitas una espera explícita, espera por una condición específica: await page.waitForResponse(), await locator.waitFor() o await expect(locator).toBeVisible().
Probar detalles de implementación. Tu prueba E2E debe validar lo que el usuario ve y hace, no cómo el frontend lo renderiza internamente. Hacer aserciones sobre clases CSS, estado interno de componentes o estructura del DOM acopla tus pruebas a decisiones de implementación que cambiarán.
Sobreusar E2E para lo que deberían cubrir las pruebas unitarias. Si estás escribiendo una prueba E2E para verificar que una función utilitaria formatea una fecha correctamente, estás usando el nivel de testing incorrecto. Las pruebas E2E deben cubrir recorridos de usuario a través de la aplicación. La validación de lógica pertenece a las pruebas unitarias.
Ignorar la pirámide de pruebas. Aún veo equipos con 200 pruebas E2E y 10 pruebas unitarias. Esta pirámide invertida significa ciclos de retroalimentación lentos, altos costos de mantenimiento y resultados de pruebas frágiles. Una proporción saludable para la mayoría de aplicaciones web es aproximadamente 70% unitarias, 20% integración, 10% E2E.
Playwright te da las herramientas para construir una suite de pruebas E2E confiable. Pero las herramientas no imponen disciplina: las prácticas de ingeniería sí. Estructura tu proyecto para el crecimiento, diseña Page Objects alrededor de intenciones del usuario, aprovecha los fixtures para aislamiento, elige selectores resilientes e intégrate de manera reflexiva en CI. El resultado es una suite de pruebas en la que los desarrolladores confían y mantienen activamente, en lugar de una que aprenden a ignorar.CI pipelines. The result is a test suite that developers trust and actively maintain, rather than one they learn to ignore.
Comentarios
0 comentariosTodos los comentarios son moderados y aparecerán después de la revisión.