testingplaywrighte2ecalidad

Testing de E2E con Playwright: Más Allá de los Clicks

Por Binary Core

Playwright se ha convertido en el estándar de facto para testing E2E. Pero más allá de clicks simples, Playwright ofrece features poderosas para crear suites de tests robustas y mantenibles. En Binary Core, hemos desarrollado patrones que nos permiten confiar en nuestros tests E2E.

Page Objects Pattern

El patrón Page Objects es fundamental para mantener tests E2E mantenibles:

typescript
// pages/LoginPage.ts import { Page, expect } from '@playwright/test'; export class LoginPage { constructor(private page: Page) {} async goto() { await this.page.goto('/login'); } async login(email: string, password: string) { await this.page.fill('[name="email"]', email); await this.page.fill('[name="password"]', password); await this.page.click('[type="submit"]'); } async expectLoggedIn() { await expect(this.page).toHaveURL('/dashboard'); await expect(this.page.locator('[data-testid="user-menu"]')).toBeVisible(); } async expectError(message: string) { await expect(this.page.locator('[data-testid="error"]')).toHaveText(message); } }

Test usando Page Objects

typescript
// tests/login.spec.ts import { test, expect } from '@playwright/test'; import { LoginPage } from '../pages/LoginPage'; test('login exitoso', async ({ page }) => { const loginPage = new LoginPage(page); await loginPage.goto(); await loginPage.login('user@example.com', 'password123'); await loginPage.expectLoggedIn(); }); test('login con credenciales inválidas', async ({ page }) => { const loginPage = new LoginPage(page); await loginPage.goto(); await loginPage.login('user@example.com', 'wrong-password'); await loginPage.expectError('Credenciales inválidas'); });

Visual Regression Testing

Playwright tiene soporte nativo para visual regression testing:

typescript
// tests/visual.spec.ts import { test, expect } from '@playwright/test'; test.describe('Visual Regression', () => { test('homepage visual', async ({ page }) => { await page.goto('/'); // Captura completa await expect(page).toHaveScreenshot('homepage.png'); }); test('component visual', async ({ page }) => { await page.goto('/'); // Captura de elemento específico const hero = page.locator('[data-testid="hero"]'); await expect(hero).toHaveScreenshot('hero.png'); }); test('dark mode visual', async ({ page }) => { await page.goto('/'); await page.click('[data-testid="theme-toggle"]'); // Captura con diferentes viewports await expect(page).toHaveScreenshot('homepage-dark.png', { fullPage: true, }); }); });

Configuración para Visual Regression

typescript
// playwright.config.ts import { defineConfig, devices } from '@playwright/test'; export default defineConfig({ use: { screenshot: 'only-on-failure', video: 'retain-on-failure', trace: 'retain-on-failure', }, projects: [ { name: 'chromium', use: { ...devices['Desktop Chrome'] }, }, { name: 'firefox', use: { ...devices['Desktop Firefox'] }, }, { name: 'webkit', use: { ...devices['Desktop Safari'] }, }, ], });

Testing de APIs

Playwright no es solo para UI — también puedes testear APIs:

typescript
// tests/api.spec.ts import { test, expect } from '@playwright/test'; test.describe('API Testing', () => { const API_URL = 'https://api.example.com'; test('GET /users retorna lista', async ({ request }) => { const response = await request.get(`${API_URL}/users`); expect(response.status()).toBe(200); const users = await response.json(); expect(Array.isArray(users)).toBe(true); expect(users.length).toBeGreaterThan(0); }); test('POST /users crea usuario', async ({ request }) => { const newUser = { email: 'test@example.com', name: 'Test User', }; const response = await request.post(`${API_URL}/users`, { data: newUser, }); expect(response.status()).toBe(201); const user = await response.json(); expect(user.email).toBe(newUser.email); expect(user.id).toBeDefined(); }); test('PUT /users/:id actualiza usuario', async ({ request }) => { // Primero crear usuario const createResponse = await request.post(`${API_URL}/users`, { data: { email: 'test@example.com', name: 'Test User' }, }); const user = await createResponse.json(); // Actualizar const updateResponse = await request.put(`${API_URL}/users/${user.id}`, { data: { name: 'Updated Name' }, }); expect(updateResponse.status()).toBe(200); const updatedUser = await updateResponse.json(); expect(updatedUser.name).toBe('Updated Name'); }); });

Fixtures Personalizados

Crea fixtures reutilizables para tu lógica de test:

typescript
// tests/fixtures.ts import { test as base } from '@playwright/test'; import { LoginPage } from '../pages/LoginPage'; type MyFixtures = { authenticatedPage: LoginPage; apiBaseUrl: string; }; export const test = base.extend<MyFixtures>({ authenticatedPage: async ({ page }, use) => { const loginPage = new LoginPage(page); await loginPage.goto(); await loginPage.login('test@example.com', 'password'); await use(loginPage); }, apiBaseUrl: async ({}, use) => { await use(process.env.API_URL || 'http://localhost:3000/api'); }, }); export { expect } from '@playwright/test';

Usando Fixtures Personalizados

typescript
// tests/dashboard.spec.ts import { test, expect } from './fixtures'; test('dashboard muestra datos', async ({ authenticatedPage }) => { // Ya está autenticado await authenticatedPage.page.goto('/dashboard'); await expect(authenticatedPage.page.locator('[data-testid="stats"]')).toBeVisible(); });

Debugging de Tests Flaky

Los tests flaky son el enemigo de la confianza en E2E. Aquí estrategias para combatirlos:

1. Esperas Explícitas vs Implícitas

typescript
// ❌ Mal: esperas fijas test('mal ejemplo', async ({ page }) => { await page.click('button'); await page.waitForTimeout(1000); // NO HACER ESTO await expect(page.locator('.result')).toBeVisible(); }); // ✅ Bien: esperas basadas en condiciones test('buen ejemplo', async ({ page }) => { await page.click('button'); await expect(page.locator('.result')).toBeVisible(); });

2. Selectores Estables

typescript
// ❌ Mal: selectores frágiles await page.click('div > div > button'); await page.click('.btn-primary'); await page.click('#submit-btn-123'); // ✅ Bien: selectores semánticos await page.click('[data-testid="submit-button"]'); await page.getByRole('button', { name: 'Enviar' }).click(); await page.getByLabel('Email').fill('test@example.com');

3. Retry de Acciones

typescript
// playwright.config.ts export default defineConfig({ use: { actionTimeout: 10000, navigationTimeout: 30000, }, }); // En el test test('con retry automático', async ({ page }) => { // Playwright reintenta automáticamente si falla await page.click('[data-testid="submit-button"]'); });

4. Aislamiento de Tests

typescript
// Cada test debe ser independiente test('test 1', async ({ page }) => { await page.goto('/test'); // Setup específico await page.click('[data-testid="setup"]'); // Test await expect(page.locator('.result')).toBeVisible(); // Cleanup automático por contexto nuevo }); test('test 2', async ({ page }) => { // Contexto nuevo, sin estado del test anterior await page.goto('/test'); await expect(page.locator('.result')).not.toBeVisible(); });

Testing de Accesibilidad

Playwright tiene integración con axe-core para testing de accesibilidad:

typescript
// tests/a11y.spec.ts import { test, expect } from '@playwright/test'; import { injectAxe, checkA11y } from 'axe-playwright'; test.describe('Accesibilidad', () => { test('homepage es accesible', async ({ page }) => { await page.goto('/'); await injectAxe(page); await checkA11y(page); }); test('formulario es accesible', async ({ page }) => { await page.goto('/contact'); await injectAxe(page); // Excluir elementos específicos await checkA11y(page, null, { detailedReport: true, detailedReportOptions: { html: true }, }); }); });

Ejecución en Paralelo

Playwright ejecuta tests en paralelo por defecto. Configura workers según tu recursos:

typescript
// playwright.config.ts export default defineConfig({ workers: process.env.CI ? 2 : 4, // Menos workers en CI fullyParallel: true, retries: process.env.CI ? 2 : 0, // Reintentos en CI });

Sharding para Escalabilidad

bash
# Ejecutar tests en múltiples máquinas npx playwright test --shard=1/4 npx playwright test --shard=2/4 npx playwright test --shard=3/4 npx playwright test --shard=4/4

CI/CD Integration

GitHub Actions

yaml
# .github/workflows/e2e.yml name: E2E Tests on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: node-version: '18' - run: npm ci - run: npm run build - run: npx playwright install --with-deps - run: npx playwright test - uses: actions/upload-artifact@v3 if: always() with: name: playwright-report path: playwright-report/

Mejores Prácticas de Binary Core

  1. Siempre usa data-testid para selectores de test
  2. Page Objects para páginas complejas con múltiples interacciones
  3. Visual regression solo para componentes críticos (no todo)
  4. API tests antes de UI tests — son más rápidos y confiables
  5. Nunca uses waitForTimeout — usa expect con esperas automáticas
  6. Cada test debe ser independiente — no dependas de otros tests
  7. Ejecuta tests en local antes de commit — ahorra tiempo en CI
  8. Revisa traces de tests fallidos — Playwright guarda traces automáticamente

Conclusión

Playwright es mucho más que una herramienta de clicks. Con page objects, visual regression, API testing y fixtures personalizados, puedes crear suites de E2E que realmente confíes. La clave es escribir tests que sean mantenibles, aislados y que usen las features modernas de Playwright en lugar de anti-patrones como esperas fijas.

En Binary Core, nuestros tests E2E nos han dado la confianza de desplegar múltiples veces al día sabiendo que flujos críticos están cubiertos. Con las prácticas correctas, tú también puede lograr lo mismo.

Binary Core

Equipo Binary Core

← Volver al blog