TechLead

Testing End-to-End

Testea flujos de usuario completos con Playwright y Cypress: automatización de navegador, interacciones de usuario y escenarios del mundo real

¿Qué es el Testing E2E?

El testing End-to-End (E2E) simula escenarios de usuario reales testeando todo el stack de tu aplicación desde la interfaz de usuario hasta los sistemas backend. Estos tests se ejecutan en navegadores reales, haciendo clic en botones, llenando formularios y verificando resultados tal como lo haría un usuario real.

🌐 Alcance del Test E2E:

Los tests E2E validan flujos de usuario completos: autenticación, navegación, entrada de datos, llamadas a API, cambios en base de datos y actualizaciones de UI. Proporcionan la mayor confianza pero son los más lentos y costosos de mantener.

Cuándo Usar Tests E2E

✅ Buenos Casos de Uso

  • • Flujos críticos de usuario (registro, checkout)
  • • Funcionalidades generadoras de ingresos
  • • Flujos de trabajo complejos multi-paso
  • • Autenticación y autorización
  • • Compatibilidad entre navegadores
  • • Validación de happy path

❌ Casos de Uso Pobres

  • • Testear lógica de negocio (usa tests unitarios)
  • • Casos extremos y manejo de errores
  • • Reglas de validación de datos
  • • Cada posible ruta de usuario
  • • Variaciones de renderizado de componentes
  • • Funciones utilitarias

Playwright vs Cypress

🎭 Playwright

Moderno, rápido, soporta múltiples navegadores

  • ✓ Chrome, Firefox, Safari, Edge
  • ✓ Ejecución en paralelo
  • ✓ Auto-wait para elementos
  • ✓ Interceptación de red
  • ✓ Emulación móvil

Mejor para: Soporte multi-navegador, tests paralelos, apps modernas

🌲 Cypress

Amigable para desarrolladores, gran DX, recarga en tiempo real

  • ✓ Chrome, Firefox, Edge
  • ✓ Depuración con viaje en el tiempo
  • ✓ Screenshots/videos automáticos
  • ✓ Excelentes mensajes de error
  • ✓ Fácil de empezar

Mejor para: Configuración rápida, experiencia de desarrollador, enfocado en Chrome

Ejemplo E2E con Playwright

// playwright.config.js - Configuración
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './e2e',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: 'html',
  use: {
    baseURL: 'http://localhost:3000',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
  },

  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
    },
  ],

  webServer: {
    command: 'npm run dev',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
  },
});
// e2e/login.spec.js - Test de flujo de login
import { test, expect } from '@playwright/test';

test.describe('Flujo de Login', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('/');
  });

  test('login exitoso redirige al dashboard', async ({ page }) => {
    // Navegar a login
    await page.click('text=Login');
    await expect(page).toHaveURL('/login');

    // Llenar formulario de login
    await page.fill('[name="email"]', 'user@example.com');
    await page.fill('[name="password"]', 'password123');
    
    // Enviar formulario
    await page.click('button[type="submit"]');

    // Esperar navegación
    await page.waitForURL('/dashboard');
    
    // Verificar sesión iniciada
    await expect(page.locator('text=Bienvenido de nuevo')).toBeVisible();
    await expect(page.locator('[data-testid="user-menu"]')).toBeVisible();
  });

  test('muestra error en credenciales inválidas', async ({ page }) => {
    await page.goto('/login');

    await page.fill('[name="email"]', 'wrong@example.com');
    await page.fill('[name="password"]', 'wrongpassword');
    await page.click('button[type="submit"]');

    // Debería permanecer en página de login
    await expect(page).toHaveURL('/login');
    
    // Debería mostrar mensaje de error
    await expect(page.locator('text=Credenciales inválidas')).toBeVisible();
  });

  test('valida campos requeridos', async ({ page }) => {
    await page.goto('/login');

    // Intentar enviar formulario vacío
    await page.click('button[type="submit"]');

    // Verificar mensajes de validación
    await expect(page.locator('text=Email es requerido')).toBeVisible();
    await expect(page.locator('text=Contraseña es requerida')).toBeVisible();
  });

  test('recuerda usuario después de recargar página', async ({ page, context }) => {
    await page.goto('/login');
    
    // Login
    await page.fill('[name="email"]', 'user@example.com');
    await page.fill('[name="password"]', 'password123');
    await page.click('button[type="submit"]');
    await page.waitForURL('/dashboard');

    // Recargar página
    await page.reload();

    // Debería seguir con sesión iniciada
    await expect(page).toHaveURL('/dashboard');
    await expect(page.locator('text=Bienvenido de nuevo')).toBeVisible();
  });
});

Flujo de Checkout E-Commerce

// e2e/checkout.spec.js - Flujo de compra completa
import { test, expect } from '@playwright/test';

test.describe('Checkout E-Commerce', () => {
  test('usuario puede completar compra completa', async ({ page }) => {
    // 1. Explorar productos
    await page.goto('/');
    await expect(page.locator('h1')).toContainText('Tienda');

    // 2. Agregar items al carrito
    await page.click('[data-product-id="1"] button:has-text("Agregar al Carrito")');
    await page.click('[data-product-id="3"] button:has-text("Agregar al Carrito")');

    // Verificar que el badge del carrito se actualiza
    await expect(page.locator('[data-testid="cart-count"]')).toHaveText('2');

    // 3. Ver carrito
    await page.click('[data-testid="cart-button"]');
    await expect(page).toHaveURL('/cart');
    
    // Verificar items en carrito
    await expect(page.locator('[data-testid="cart-item"]')).toHaveCount(2);
    
    // Verificar total
    const total = await page.locator('[data-testid="cart-total"]').textContent();
    expect(total).toContain('$');

    // 4. Proceder a checkout
    await page.click('text=Proceder al Checkout');
    await expect(page).toHaveURL('/checkout');

    // 5. Llenar información de envío
    await page.fill('[name="fullName"]', 'Juan Pérez');
    await page.fill('[name="email"]', 'juan@example.com');
    await page.fill('[name="address"]', 'Calle Principal 123');
    await page.fill('[name="city"]', 'Madrid');
    await page.fill('[name="zipCode"]', '28001');
    await page.selectOption('[name="country"]', 'ES');

    // 6. Continuar a pago
    await page.click('text=Continuar al Pago');

    // 7. Llenar información de pago
    await page.fill('[name="cardNumber"]', '4242424242424242');
    await page.fill('[name="cardExpiry"]', '12/25');
    await page.fill('[name="cardCvc"]', '123');
    await page.fill('[name="cardName"]', 'Juan Pérez');

    // 8. Realizar orden
    await page.click('text=Realizar Orden');

    // 9. Verificar confirmación de orden
    await expect(page).toHaveURL(/\/order-confirmation/);
    await expect(page.locator('h1')).toContainText('Gracias');
    await expect(page.locator('text=Orden #')).toBeVisible();

    // Verificar detalles de orden
    await expect(page.locator('text=Juan Pérez')).toBeVisible();
    await expect(page.locator('text=Calle Principal 123')).toBeVisible();
    await expect(page.locator('[data-testid="order-item"]')).toHaveCount(2);
  });

  test('valida formulario de envío', async ({ page }) => {
    await page.goto('/checkout');

    // Intentar enviar sin llenar
    await page.click('text=Continuar al Pago');

    // Debería mostrar errores de validación
    await expect(page.locator('text=Nombre es requerido')).toBeVisible();
    await expect(page.locator('text=Email es requerido')).toBeVisible();
    await expect(page.locator('text=Dirección es requerida')).toBeVisible();
  });

  test('puede aplicar código de descuento', async ({ page }) => {
    // Agregar item e ir al carrito
    await page.goto('/');
    await page.click('[data-product-id="1"] button:has-text("Agregar al Carrito")');
    await page.click('[data-testid="cart-button"]');

    // Obtener total original
    const originalTotal = await page.locator('[data-testid="cart-total"]').textContent();

    // Aplicar código de descuento
    await page.fill('[name="discountCode"]', 'SAVE10');
    await page.click('text=Aplicar');

    // Verificar descuento aplicado
    await expect(page.locator('text=Descuento aplicado')).toBeVisible();
    
    // Verificar nuevo total es menor
    const newTotal = await page.locator('[data-testid="cart-total"]').textContent();
    expect(parseFloat(newTotal.replace('$', ''))).toBeLessThan(
      parseFloat(originalTotal.replace('$', ''))
    );
  });
});

Ejemplo E2E con Cypress

// cypress.config.js - Configuración
const { defineConfig } = require('cypress');

module.exports = defineConfig({
  e2e: {
    baseUrl: 'http://localhost:3000',
    setupNodeEvents(on, config) {},
    viewportWidth: 1280,
    viewportHeight: 720,
    video: true,
    screenshotOnRunFailure: true,
  },
});
// cypress/e2e/todo-app.cy.js - Test de app de tareas
describe('Aplicación de Tareas', () => {
  beforeEach(() => {
    cy.visit('/');
  });

  it('muestra estado vacío inicialmente', () => {
    cy.contains('No hay tareas aún').should('be.visible');
  });

  it('puede agregar nuevas tareas', () => {
    // Agregar primera tarea
    cy.get('[data-testid="todo-input"]').type('Comprar leche');
    cy.get('[data-testid="add-button"]').click();

    // Verificar que aparece la tarea
    cy.contains('Comprar leche').should('be.visible');
    
    // Verificar que el estado vacío desapareció
    cy.contains('No hay tareas aún').should('not.exist');

    // Agregar segunda tarea
    cy.get('[data-testid="todo-input"]').type('Pasear perro');
    cy.get('[data-testid="add-button"]').click();

    // Verificar ambas tareas
    cy.get('[data-testid="todo-item"]').should('have.length', 2);
  });

  it('puede marcar tareas como completadas', () => {
    // Agregar tarea
    cy.get('[data-testid="todo-input"]').type('Comprar leche');
    cy.get('[data-testid="add-button"]').click();

    // Marcar como completada
    cy.get('[data-testid="todo-checkbox"]').check();

    // Verificar estilo completado
    cy.contains('Comprar leche')
      .should('have.css', 'text-decoration')
      .and('include', 'line-through');
  });

  it('puede eliminar tareas', () => {
    // Agregar dos tareas
    cy.get('[data-testid="todo-input"]').type('Comprar leche');
    cy.get('[data-testid="add-button"]').click();
    cy.get('[data-testid="todo-input"]').type('Pasear perro');
    cy.get('[data-testid="add-button"]').click();

    // Eliminar primera tarea
    cy.get('[data-testid="delete-button"]').first().click();

    // Verificar que solo queda una
    cy.get('[data-testid="todo-item"]').should('have.length', 1);
    cy.contains('Comprar leche').should('not.exist');
    cy.contains('Pasear perro').should('be.visible');
  });

  it('puede filtrar tareas', () => {
    // Agregar tareas completadas y activas
    cy.get('[data-testid="todo-input"]').type('Comprar leche');
    cy.get('[data-testid="add-button"]').click();
    cy.get('[data-testid="todo-input"]').type('Pasear perro');
    cy.get('[data-testid="add-button"]').click();
    
    cy.get('[data-testid="todo-checkbox"]').first().check();

    // Filtrar activas
    cy.get('[data-testid="filter-active"]').click();
    cy.get('[data-testid="todo-item"]').should('have.length', 1);
    cy.contains('Pasear perro').should('be.visible');
    cy.contains('Comprar leche').should('not.exist');

    // Filtrar completadas
    cy.get('[data-testid="filter-completed"]').click();
    cy.get('[data-testid="todo-item"]').should('have.length', 1);
    cy.contains('Comprar leche').should('be.visible');
    cy.contains('Pasear perro').should('not.exist');

    // Mostrar todas
    cy.get('[data-testid="filter-all"]').click();
    cy.get('[data-testid="todo-item"]').should('have.length', 2);
  });

  it('persiste tareas después de recargar', () => {
    // Agregar tarea
    cy.get('[data-testid="todo-input"]').type('Comprar leche');
    cy.get('[data-testid="add-button"]').click();

    // Recargar página
    cy.reload();

    // La tarea debería seguir ahí
    cy.contains('Comprar leche').should('be.visible');
  });
});

Patrones E2E Avanzados

Comandos Personalizados (Cypress)

// cypress/support/commands.js
Cypress.Commands.add('login', (email, password) => {
  cy.visit('/login');
  cy.get('[name="email"]').type(email);
  cy.get('[name="password"]').type(password);
  cy.get('button[type="submit"]').click();
  cy.url().should('include', '/dashboard');
});

Cypress.Commands.add('addToCart', (productId) => {
  cy.get('[data-product-id="' + productId + '"]')
    .find('button:contains("Agregar al Carrito")')
    .click();
  cy.get('[data-testid="cart-count"]').should('exist');
});

// Uso en tests
describe('Compras', () => {
  it('usuario con sesión puede hacer checkout', () => {
    cy.login('user@example.com', 'password123');
    cy.addToCart('product-1');
    cy.addToCart('product-2');
    cy.get('[data-testid="cart-button"]').click();
    // ... continuar con checkout
  });
});

Fixtures y Datos de Test

// cypress/fixtures/users.json
{
  "admin": {
    "email": "admin@example.com",
    "password": "admin123",
    "role": "admin"
  },
  "regularUser": {
    "email": "user@example.com",
    "password": "user123",
    "role": "user"
  }
}

// Usando fixtures
describe('Roles de Usuario', () => {
  it('admin puede acceder a panel de admin', () => {
    cy.fixture('users').then((users) => {
      cy.login(users.admin.email, users.admin.password);
      cy.visit('/admin');
      cy.contains('Panel de Admin').should('be.visible');
    });
  });

  it('usuario regular no puede acceder a panel de admin', () => {
    cy.fixture('users').then((users) => {
      cy.login(users.regularUser.email, users.regularUser.password);
      cy.visit('/admin');
      cy.contains('Acceso Denegado').should('be.visible');
    });
  });
});

Mocking e Interceptación de API

// Playwright - Simular respuestas de API
test('muestra estado de carga y luego datos', async ({ page }) => {
  // Interceptar llamada API y simular respuesta
  await page.route('**/api/users', (route) => {
    route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify([
        { id: 1, name: 'Alice' },
        { id: 2, name: 'Bob' },
      ]),
    });
  });

  await page.goto('/users');

  // Debería mostrar cargando primero
  await expect(page.locator('text=Cargando...')).toBeVisible();

  // Luego mostrar datos
  await expect(page.locator('text=Alice')).toBeVisible();
  await expect(page.locator('text=Bob')).toBeVisible();
});

// Cypress - Interceptar y simular API
describe('Integración de API', () => {
  it('maneja errores de API con gracia', () => {
    // Simular API para devolver error
    cy.intercept('GET', '/api/users', {
      statusCode: 500,
      body: { error: 'Error del servidor' },
    }).as('getUsers');

    cy.visit('/users');
    cy.wait('@getUsers');

    // Debería mostrar mensaje de error
    cy.contains('Fallo al cargar usuarios').should('be.visible');
  });

  it('puede reintentar peticiones fallidas', () => {
    let callCount = 0;
    
    // Fallar primeras dos veces, éxito tercera vez
    cy.intercept('GET', '/api/data', (req) => {
      callCount++;
      if (callCount < 3) {
        req.reply({ statusCode: 500 });
      } else {
        req.reply({ statusCode: 200, body: { data: 'éxito' } });
      }
    }).as('getData');

    cy.visit('/data');
    cy.get('[data-testid="retry-button"]').click();
    cy.get('[data-testid="retry-button"]').click();
    
    cy.contains('éxito').should('be.visible');
  });
});

Testing Móvil y Responsive

// Playwright - Testear diferentes viewports
test.describe('Diseño Responsive', () => {
  test('menú móvil funciona en pantallas pequeñas', async ({ page }) => {
    // Establecer viewport móvil
    await page.setViewportSize({ width: 375, height: 667 });
    await page.goto('/');

    // Menú móvil debería estar oculto inicialmente
    await expect(page.locator('[data-testid="mobile-menu"]')).not.toBeVisible();

    // Hacer clic en hamburguesa para abrir
    await page.click('[data-testid="hamburger"]');
    await expect(page.locator('[data-testid="mobile-menu"]')).toBeVisible();

    // La navegación debería funcionar
    await page.click('[data-testid="mobile-menu"] a:has-text("Acerca de")');
    await expect(page).toHaveURL('/about');
  });

  test('navegación de escritorio en pantallas grandes', async ({ page }) => {
    // Establecer viewport de escritorio
    await page.setViewportSize({ width: 1920, height: 1080 });
    await page.goto('/');

    // Nav de escritorio debería ser visible
    await expect(page.locator('[data-testid="desktop-nav"]')).toBeVisible();
    
    // Hamburguesa no debería existir
    await expect(page.locator('[data-testid="hamburger"]')).not.toBeVisible();
  });
});

// Cypress - Emulación móvil
describe('Experiencia Móvil', () => {
  beforeEach(() => {
    cy.viewport('iphone-x');
  });

  it('muestra diseño optimizado para móvil', () => {
    cy.visit('/');
    
    cy.get('[data-testid="mobile-layout"]').should('be.visible');
    cy.get('[data-testid="desktop-layout"]').should('not.be.visible');
  });
});

⚠️ Trampas del Testing E2E

  • Ejecución lenta: Los tests E2E pueden tomar minutos en ejecutarse
  • Inestabilidad: Problemas de red, timing, delays de animaciones
  • Mantenimiento costoso: Los cambios de UI rompen muchos tests
  • Difícil de depurar: Las fallas pueden estar en cualquier parte del stack
  • Configuración de entorno: Requiere ejecutar aplicación completa
  • Gestión de datos de test: Necesita datos de test consistentes y limpios

💡 Mejores Prácticas de Testing E2E

  • ✓ Testea solo flujos críticos de usuario (happy paths)
  • ✓ Usa atributos de datos ([data-testid]) en lugar de selectores CSS
  • ✓ Ejecuta tests E2E en pipeline CI/CD antes del despliegue
  • ✓ Mantén tests independientes - cada uno debería configurar su propio estado
  • ✓ Usa funcionalidades de auto-espera para evitar esperas manuales
  • ✓ Simula servicios externos que no controlas
  • ✓ Toma screenshots/videos en fallas para depuración
  • ✓ Paraleliza tests cuando sea posible para ahorrar tiempo
  • ✓ Enfócate en comportamiento de usuario, no detalles de implementación

📚 Más Temas de Testing

Explora los 6 temas de testing para construir una comprensión completa del testing de software.

Ver Todos los Temas