Saltar a contenido

La Piramide de Testing Moderna


Proposito

Guia practica sobre como distribuir los tests en un proyecto. Que testear en cada nivel, cuanto testear, y como tomar decisiones sobre donde invertir esfuerzo de testing.

Cuando usarlo

  • Al definir la estrategia de testing de una nueva feature.
  • Cuando un test se siente "raro" y no sabes en que nivel deberia estar.
  • Para justificar ante el equipo por que NO escribir ciertos tests.

1. La Piramide

          /\
         /  \
        / E2E \          Pocos (5-10%)
       / tests \         Flujos criticos completos
      /----------\
     /             \
    / Integration    \    Moderados (20-30%)
   /   tests          \   Componente + dependencias
  /--------------------\
 /                      \
/      Unit tests        \   Muchos (60-70%)
/_________________________ \  Logica de negocio pura

2. Unit Tests (60-70% del esfuerzo)

Que testear

  • Reglas de negocio puras (validaciones, calculos, state machines).
  • Domain services (AvailabilityCalculator, PenalizationService).
  • Value objects (TimeRange, BookingStatus transiciones).
  • Funciones utilitarias con logica no trivial.

Que NO testear

  • Getters y setters triviales.
  • Constructores sin logica.
  • Codigo que solo delega a otro componente.
  • Frameworks (no testees que NestJS rutea bien, eso lo testea NestJS).

Ejemplo concreto (Sistema de Turnos)

// SI testear: regla de negocio
describe('PenalizationService', () => {
  it('aplica penalizacion si cancela con menos de 24h', () => {
    const bookingDate = new Date('2025-03-25T09:00:00');
    const cancelDate = new Date('2025-03-24T10:00:00'); // 23h antes

    const result = service.shouldApplyPenalty(bookingDate, cancelDate);

    expect(result).toBe(true);
  });

  it('no aplica penalizacion si cancela con mas de 24h', () => {
    const bookingDate = new Date('2025-03-26T09:00:00');
    const cancelDate = new Date('2025-03-24T08:00:00'); // 49h antes

    const result = service.shouldApplyPenalty(bookingDate, cancelDate);

    expect(result).toBe(false);
  });
});

// NO testear: getter trivial
// ❌ it('returns the patient id', () => { expect(booking.patientId).toBe(...) })

Caracteristicas de un buen unit test

  • Rapido: < 10ms por test.
  • Aislado: No depende de BD, red, filesystem, ni otros tests.
  • Determinista: Mismo resultado siempre (no depende de la hora real, usa mocks de reloj).
  • Legible: El nombre del test dice que se testea y que se espera.

3. Integration Tests (20-30% del esfuerzo)

Que testear

  • Endpoints de API con BD real (o en memoria).
  • Queries complejas (busqueda de disponibilidad con joins).
  • Transacciones (creacion de booking con lock).
  • Interaccion entre servicios internos.

Que NO testear

  • Logica que ya esta cubierta por unit tests.
  • Cada permutacion de input (eso es para unit tests).
  • Servicios externos reales (mockea SendGrid, no lo llames de verdad).

Ejemplo concreto

describe('POST /api/v1/bookings', () => {
  it('crea booking y retorna 201 con datos correctos', async () => {
    // Arrange: seed doctor, availability, patient en BD de test
    const doctor = await seedDoctor({ specialty: 'Cardiologia' });
    await seedAvailability({ doctorId: doctor.id, day: 'MONDAY', start: '09:00', end: '13:00' });
    const token = await loginAsPatient();

    // Act
    const response = await request(app)
      .post('/api/v1/bookings')
      .set('Authorization', `Bearer ${token}`)
      .send({
        doctorId: doctor.id,
        clinicId: doctor.clinicId,
        specialtyId: doctor.specialtyId,
        date: '2025-03-24',
        startTime: '09:00'
      });

    // Assert
    expect(response.status).toBe(201);
    expect(response.body.data.status).toBe('CONFIRMED');

    // Verificar en BD
    const booking = await bookingRepo.findOne(response.body.data.id);
    expect(booking).toBeDefined();
    expect(booking.status).toBe('CONFIRMED');
  });
});

Caracteristicas

  • BD de test: Base de datos real (o testcontainers) que se limpia entre tests.
  • Moderadamente rapidos: < 1s por test.
  • Cubren el contrato: Validan que el endpoint se comporta como dice el API contract.

4. E2E Tests (5-10% del esfuerzo)

Que testear

  • Los 3-5 flujos mas criticos del negocio (el "money path").
  • Flujos que involucran multiples servicios o pasos.

Que NO testear

  • Variaciones del mismo flujo (eso es para integration).
  • UI pixel-perfect (usa visual regression tests separados).
  • Todo (los E2E son caros de mantener, se selectivo).

Ejemplo concreto

describe('Flujo completo de reserva', () => {
  it('paciente se registra, verifica email, busca medico, y reserva turno', async () => {
    // 1. Registro
    const registerRes = await request(app).post('/api/v1/auth/register').send({...});
    expect(registerRes.status).toBe(201);

    // 2. Verificacion (simular click en email)
    const verifyRes = await request(app).post('/api/v1/auth/verify-email').send({
      token: capturedEmailToken
    });
    expect(verifyRes.status).toBe(200);

    // 3. Login
    const loginRes = await request(app).post('/api/v1/auth/login').send({...});
    const token = loginRes.body.data.accessToken;

    // 4. Buscar disponibilidad
    const availRes = await request(app)
      .get('/api/v1/doctors/123/availability?dateFrom=2025-03-24&dateTo=2025-03-30')
      .set('Authorization', `Bearer ${token}`);
    expect(availRes.body.data.slots.length).toBeGreaterThan(0);

    // 5. Reservar turno
    const slot = availRes.body.data.slots[0];
    const bookingRes = await request(app)
      .post('/api/v1/bookings')
      .set('Authorization', `Bearer ${token}`)
      .send({ doctorId: '123', date: slot.date, startTime: slot.startTime, ... });
    expect(bookingRes.status).toBe(201);
    expect(bookingRes.body.data.status).toBe('CONFIRMED');
  });
});

Caracteristicas

  • Lentos: Pueden tomar 5-30s por test.
  • Fragiles: Se rompen mas facilmente con cambios de UI o API.
  • Valiosos: Dan confianza de que el sistema funciona de punta a punta.
  • Pocos: Selecciona solo los flujos criticos.

5. Tests que NO son parte de la piramide (pero son importantes)

Contract Tests

Verifican que el API contract se cumple. Utiles cuando frontend y backend evolucionan por separado.

Smoke Tests

Un subset minimo de E2E que corren en produccion despues de un deploy. "El sistema esta vivo?"

Load / Performance Tests

Verifican comportamiento bajo carga. No corren en CI normal, se ejecutan en etapas especificas.

Security Tests (SAST/DAST)

Analisis estatico y dinamico de seguridad. Se integran en el pipeline CI.


6. Reglas Practicas

  1. Si el test no falla cuando rompes el codigo, no sirve. Rompe intencionalmente la logica y verifica que el test falla.
  2. Si el test se rompe con cada refactor, esta testeando implementacion, no comportamiento. Reescribelo.
  3. Si no sabes que testear, empieza por las reglas de negocio. Son las mas valiosas.
  4. Un test lento es un test que nadie corre. Mantene los unit tests rapidos.
  5. Coverage no es calidad. 80% de coverage con tests triviales vale menos que 50% con tests que cubren reglas de negocio.

7. Coverage Guidelines

Zona Target Por que
Domain services / reglas de negocio > 90% Es donde estan los bugs mas caros
Controllers / endpoints > 70% Cubrir happy path + errores principales
Config / bootstrap No testear Codigo de setup, no logica
Utilities genericas > 80% Si tienen logica, testear. Si son wrappers, no.

Checklist de Completitud

  • Piramide explicada con porcentajes
  • Que testear / que no en cada nivel
  • Ejemplos concretos del dominio de turnos
  • Reglas practicas documentadas
  • Coverage guidelines definidas
  • Revisada por otro ingeniero

Archivos relacionados