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
Aprende Más
📚 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