Estrategias de Pruebas - La Pirámide de Pruebas para Garantizar la Calidad

16 min de lectura | 2025.12.01

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

ElementoPruebas Unitarias
ObjetivoFunciones, clases, módulos
VelocidadMuy rápidas (milisegundos)
EstabilidadAlta
AlcanceLimitado (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

ElementoPruebas de Integración
ObjetivoAPI, integración con base de datos, servicios externos
VelocidadMedia (segundos)
EstabilidadMedia
AlcanceMedio

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

ElementoPruebas E2E
ObjetivoFlujo completo del usuario
VelocidadLenta (minutos)
EstabilidadBaja (propensas a ser flaky)
AlcanceAmplio

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 CoberturaDescripción
Cobertura de líneasPorcentaje de líneas ejecutadas
Cobertura de ramasPorcentaje de ramas ejecutadas
Cobertura de funcionesPorcentaje 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

EscenarioPruebas a Priorizar
Lógica de negocio complejaPruebas unitarias
Muchas integraciones externasPruebas de integración
UI importantePruebas E2E
Modificación de código legacyE2E 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