La Pirámide de Testing
Comprendiendo el balance óptimo de tests unitarios, de integración y E2E
¿Qué es la Pirámide de Testing?
La Pirámide de Testing es un framework que te ayuda a crear una suite de tests balanceada y eficiente. Sugiere tener muchos tests unitarios rápidos y económicos en la base, menos tests de integración en el medio, y aún menos tests end-to-end lentos y costosos en la cima.
🎯 Principio Central:
Escribe tests al nivel más bajo posible. Los tests unitarios son rápidos y identifican fallas con precisión. Los tests E2E son lentos pero proporcionan la mayor confianza. El balance es clave.
La Pirámide de Testing Tradicional
🟢 Tests Unitarios
- • Milisegundos para ejecutar
- • Testean funciones individuales
- • Fáciles de depurar
- • Más baratos de mantener
🟡 Tests de Integración
- • Segundos para ejecutar
- • Testean interacciones de módulos
- • Complejidad moderada
- • Detectan bugs de integración
🔴 Tests E2E
- • Minutos para ejecutar
- • Testean flujos completos
- • Difíciles de depurar
- • Mayor confianza
¿Por Qué Esta Forma?
Velocidad y Retroalimentación
Los tests unitarios se ejecutan en milisegundos, dándote retroalimentación instantánea. Los tests E2E pueden tomar minutos.
// Comparación de velocidad
Suite de Tests Unitarios: 0.5 segundos ✓
Tests de Integración: 15 segundos ⚡
Suite de Tests E2E: 5 minutos 🐌
Desarrollador ejecuta tests: Cada pocos minutos
Pipeline CI/CD: Cada commit
Tests rápidos = Más testing = Mejor código
Costo y Mantenimiento
Tests Unitarios: Se rompen raramente, fáciles de arreglar cuando lo hacen
Tests de Integración: Pueden ser inestables, requieren más configuración
Tests E2E: Más frágiles, se rompen con cambios de UI, timeouts, etc.
Depuración y Precisión
Cuando un test unitario falla, sabes exactamente qué función está rota. Cuando un test E2E falla, el bug podría estar en cualquier parte del stack.
Pirámide de Testing en la Práctica
// Ejemplo: Funcionalidad de Carrito de Compras E-commerce
// ✅ TESTS UNITARIOS (Muchos) - Testear funciones puras
// cart.js
export function calculateTotal(items) {
return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}
export function applyDiscount(total, discountPercent) {
return total * (1 - discountPercent / 100);
}
// cart.test.js
describe('Cálculos del carrito', () => {
test('calcula el total correctamente', () => {
const items = [
{ price: 10, quantity: 2 },
{ price: 5, quantity: 3 }
];
expect(calculateTotal(items)).toBe(35);
});
test('aplica descuento correctamente', () => {
expect(applyDiscount(100, 10)).toBe(90);
expect(applyDiscount(100, 0)).toBe(100);
});
test('maneja carrito vacío', () => {
expect(calculateTotal([])).toBe(0);
});
});
// ⚡ TESTS DE INTEGRACIÓN (Moderados) - Testear componentes trabajando juntos
// CartComponent.test.jsx
describe('Integración Componente Carrito', () => {
test('actualiza total cuando se agregan items', () => {
render( );
// Agregar items
fireEvent.click(screen.getByText('Agregar Item 1'));
fireEvent.click(screen.getByText('Agregar Item 2'));
// Verificar que el total se actualizó
expect(screen.getByText(/Total: $35/)).toBeInTheDocument();
});
test('aplica código de descuento', async () => {
render( );
// Ingresar código de descuento
fireEvent.change(screen.getByPlaceholderText('Código de descuento'), {
target: { value: 'SAVE10' }
});
fireEvent.click(screen.getByText('Aplicar'));
// Verificar que se aplicó el descuento
await waitFor(() => {
expect(screen.getByText(/Descuento: 10%/)).toBeInTheDocument();
});
});
});
// 🌐 TESTS E2E (Pocos) - Testear flujos críticos de usuario
// checkout.e2e.test.js
describe('Flujo de Checkout E2E', () => {
test('usuario puede completar compra', async () => {
// Navegar a la tienda
await page.goto('http://localhost:3000');
// Agregar items al carrito
await page.click('[data-testid="add-product-1"]');
await page.click('[data-testid="add-product-2"]');
// Ir a checkout
await page.click('[data-testid="cart-button"]');
await page.click('[data-testid="checkout-button"]');
// Llenar información de envío
await page.fill('[name="email"]', 'user@example.com');
await page.fill('[name="address"]', '123 Main St');
// Enviar pago
await page.fill('[name="cardNumber"]', '4242424242424242');
await page.click('[data-testid="submit-payment"]');
// Verificar éxito
await expect(page.locator('text=Orden confirmada')).toBeVisible();
});
});
📊 Distribución de Cobertura:
- Tests Unitarios: 20 tests cubriendo toda la lógica de cálculo (se ejecutan en 0.1s)
- Tests de Integración: 8 tests para interacciones de componentes (se ejecutan en 5s)
- Tests E2E: 3 tests solo para rutas críticas (se ejecutan en 2 min)
El Trofeo de Testing (Alternativa Moderna)
🏆 ¿Por Qué el Trofeo?
- • Tests de integración proporcionan el mejor ROI para apps frontend
- • Testean componentes como los usuarios interactúan con ellos
- • Menos mocking = tests más realistas
- • Aún lo suficientemente rápidos para ejecutar frecuentemente
Qué Testear en Cada Nivel
Tests Unitarios
- ✓ Funciones puras
- ✓ Lógica de negocio
- ✓ Utilidades y helpers
- ✓ Casos extremos
- ✓ Transformaciones de datos
- ✓ Validaciones
Tests de Integración
- ✓ Interacciones de componentes
- ✓ Integración de API
- ✓ Consultas de base de datos
- ✓ Envíos de formularios
- ✓ Enrutamiento
- ✓ Gestión de estado
Tests E2E
- ✓ Rutas críticas de usuario
- ✓ Flujos de autenticación
- ✓ Procesamiento de pagos
- ✓ Flujos multi-página
- ✓ Solo happy paths
- ✓ Funcionalidades críticas de ingresos
Anti-Patrón: El Cono de Helado
❌ Qué Evitar
Problemas:
- • Suite de tests lenta (los desarrolladores no la ejecutarán)
- • Tests inestables que fallan aleatoriamente
- • Difícil de depurar fallas
- • Costoso de mantener
- • Mala experiencia de desarrollador
Construyendo Tu Estrategia de Testing
// Enfoque paso a paso para testear una nueva funcionalidad
// 1. Comienza con tests unitarios para lógica de negocio
describe('OrderProcessor', () => {
test('calcula total de orden con impuesto', () => {
const processor = new OrderProcessor({ taxRate: 0.08 });
const order = { subtotal: 100 };
expect(processor.calculateTotal(order)).toBe(108);
});
});
// 2. Agrega tests de integración para comportamiento de componentes
describe('CheckoutForm', () => {
test('envía orden cuando el formulario es válido', async () => {
const onSubmit = jest.fn();
render( );
// Llenar formulario...
fireEvent.click(screen.getByText('Realizar Orden'));
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith(expect.objectContaining({
email: 'test@example.com',
total: 108
}));
});
});
});
// 3. Agrega UN test E2E para la ruta crítica
test('usuario puede completar checkout', async () => {
// Flujo completo de usuario desde carrito hasta confirmación
await page.goto('/cart');
await page.click('text=Checkout');
// ... llenar todos los formularios ...
await page.click('text=Realizar Orden');
await expect(page.locator('text=Gracias')).toBeVisible();
});
💡 Puntos Clave
- ✓ Escribe más tests unitarios que tests de integración, más tests de integración que E2E
- ✓ Tests rápidos se ejecutan más a menudo, proporcionando mejor retroalimentación
- ✓ Tests E2E deberían cubrir solo flujos críticos de usuario
- ✓ Tests de integración proporcionan el mejor balance para apps frontend
- ✓ Ajusta la pirámide basándote en tu tipo de aplicación
- ✓ Evita el cono de helado - demasiados tests E2E lentos
📚 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