Por Qué las Pruebas son Importantes
Las pruebas son un medio importante para garantizar la calidad del código y prevenir regresiones (bugs de retroceso).
El código sin pruebas:
- Da miedo refactorizar
- No se conoce el alcance del impacto de los cambios
- Los bugs se descubren en producción
Pirámide de Pruebas
Es un modelo que muestra los tipos de pruebas y su proporción recomendada.
flowchart TB
subgraph Pyramid["Pirámide de Pruebas"]
E2E["Pruebas E2E<br/>Pocas (10%)<br/>Alto costo, lentas, inestables"]
Integration["Pruebas de Integración<br/>Cantidad media (20%)<br/>Costo medio, velocidad media"]
Unit["Pruebas Unitarias<br/>Muchas (70%)<br/>Bajo costo, rápidas, estables"]
E2E --> Integration --> Unit
end
Pruebas Unitarias
Prueban funciones o clases individuales de forma independiente.
Características
| Elemento | Pruebas Unitarias |
|---|---|
| Objetivo | Funciones, clases, módulos |
| Velocidad | Muy rápidas (milisegundos) |
| Estabilidad | Alta |
| Alcance | Limitado (funcionalidad única) |
Ejemplo de Implementación
// Código a probar
function calculateTotal(items, taxRate) {
const subtotal = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
return Math.round(subtotal * (1 + taxRate));
}
// Pruebas
describe('calculateTotal', () => {
it('calcula el total de los productos', () => {
const items = [
{ price: 100, quantity: 2 },
{ price: 200, quantity: 1 }
];
expect(calculateTotal(items, 0.1)).toBe(440);
});
it('devuelve 0 para un array vacío', () => {
expect(calculateTotal([], 0.1)).toBe(0);
});
it('con tasa de impuesto 0% es igual al subtotal', () => {
const items = [{ price: 100, quantity: 1 }];
expect(calculateTotal(items, 0)).toBe(100);
});
});
Patrón AAA
it('crea un usuario', () => {
// Arrange (Preparar)
const userData = { name: 'Alice', email: 'alice@example.com' };
// Act (Actuar)
const user = createUser(userData);
// Assert (Afirmar)
expect(user.id).toBeDefined();
expect(user.name).toBe('Alice');
});
Pruebas de Integración
Prueban que múltiples componentes funcionen correctamente en conjunto.
Características
| Elemento | Pruebas de Integración |
|---|---|
| Objetivo | API, integración con base de datos, servicios externos |
| Velocidad | Media (segundos) |
| Estabilidad | Media |
| Alcance | Medio |
Ejemplo de Implementación (API)
describe('POST /api/users', () => {
beforeEach(async () => {
await db.users.deleteMany();
});
it('crea un nuevo usuario', async () => {
const response = await request(app)
.post('/api/users')
.send({ name: 'Alice', email: 'alice@example.com' })
.expect(201);
expect(response.body.id).toBeDefined();
expect(response.body.name).toBe('Alice');
// Confirmar que se guardó en la DB
const user = await db.users.findById(response.body.id);
expect(user).not.toBeNull();
});
it('devuelve error con email duplicado', async () => {
await db.users.create({ name: 'Bob', email: 'alice@example.com' });
await request(app)
.post('/api/users')
.send({ name: 'Alice', email: 'alice@example.com' })
.expect(409);
});
});
Pruebas E2E (End-to-End)
Prueban la aplicación completa desde la perspectiva del usuario.
Características
| Elemento | Pruebas E2E |
|---|---|
| Objetivo | Flujo completo del usuario |
| Velocidad | Lenta (minutos) |
| Estabilidad | Baja (propensas a ser flaky) |
| Alcance | Amplio |
Ejemplo de Implementación (Playwright)
import { test, expect } from '@playwright/test';
test('iniciar sesión y mostrar dashboard', async ({ page }) => {
// Navegar a la página de login
await page.goto('/login');
// Llenar el formulario
await page.fill('[name="email"]', 'user@example.com');
await page.fill('[name="password"]', 'password123');
await page.click('button[type="submit"]');
// Confirmar redirección al dashboard
await expect(page).toHaveURL('/dashboard');
await expect(page.locator('h1')).toContainText('Dashboard');
});
test('buscar producto y comprar', async ({ page }) => {
await page.goto('/');
// Buscar
await page.fill('[name="search"]', 'Laptop');
await page.click('button[type="submit"]');
// Agregar producto al carrito
await page.click('[data-testid="add-to-cart"]');
// Ir al carrito
await page.click('[data-testid="cart-icon"]');
await expect(page.locator('.cart-item')).toHaveCount(1);
});
Mocks/Stubs
Simulan dependencias externas para hacer las pruebas independientes.
// Mock de API externa
jest.mock('./paymentService', () => ({
processPayment: jest.fn().mockResolvedValue({ success: true, transactionId: 'tx_123' })
}));
import { processPayment } from './paymentService';
it('procesa el pago', async () => {
const result = await checkout(order);
expect(processPayment).toHaveBeenCalledWith({
amount: order.total,
currency: 'JPY'
});
expect(result.transactionId).toBe('tx_123');
});
Desarrollo Guiado por Pruebas (TDD)
Metodología de desarrollo donde se escriben las pruebas antes de la implementación.
flowchart LR
Red["Red<br/>Escribir prueba que falla"] --> Green["Green<br/>Implementación mínima para pasar"] --> Refactor["Refactor<br/>Mejorar el código"]
Refactor --> Red
Ejemplo de TDD
// 1. Red: Escribir prueba que falla
it('error si la contraseña tiene menos de 8 caracteres', () => {
expect(() => validatePassword('1234567')).toThrow('Password too short');
});
// 2. Green: Implementación mínima
function validatePassword(password) {
if (password.length < 8) {
throw new Error('Password too short');
}
}
// 3. Refactor: Mejorar el código si es necesario
Cobertura
Indicador que muestra qué porcentaje del código está cubierto por las pruebas.
| Tipo de Cobertura | Descripción |
|---|---|
| Cobertura de líneas | Porcentaje de líneas ejecutadas |
| Cobertura de ramas | Porcentaje de ramas ejecutadas |
| Cobertura de funciones | Porcentaje de funciones ejecutadas |
Objetivos de Cobertura
Generalmente se apunta al 80%
Sin embargo:
- No es necesario apuntar al 100%
- Alta cobertura ≠ Alta calidad de pruebas
- Priorizar cubrir los caminos importantes
Cómo Elegir una Estrategia de Pruebas
| Escenario | Pruebas a Priorizar |
|---|---|
| Lógica de negocio compleja | Pruebas unitarias |
| Muchas integraciones externas | Pruebas de integración |
| UI importante | Pruebas E2E |
| Modificación de código legacy | E2E como red de seguridad |
Resumen
Una estrategia de pruebas efectiva consiste en ser consciente de la pirámide de pruebas y distribuir equilibradamente cada nivel de pruebas. Centrándonos en las pruebas unitarias, confirmamos la integración con pruebas de integración y garantizamos los flujos de usuario importantes con pruebas E2E. Las pruebas no son algo que ralentice el desarrollo, sino una inversión para mejorar la calidad y eficiencia del desarrollo a largo plazo.
← Volver a la lista