Módulo 1: De la unidad a la integración: por qué

3. La pirámide de tests

Descripción

En la lección 2 quedó claro que quieres las dos clases de test: unitarios por su velocidad y precisión, de integración por la confianza en las juntas. La pregunta que sigue es de proporción: si escribieras cien tests para Reservo, ¿cuántos de cada clase? La respuesta más conocida de la ingeniería de pruebas tiene forma de figura geométrica —una pirámide— y no es una moda ni un dibujo bonito: es la consecuencia lógica de las propiedades que ya conoces.

La pirámide de tests dice: muchos unitarios en la base, menos de integración en el medio, pocos de punta a punta (end-to-end) en la cúspide. La base es ancha porque los unit tests son baratos —rápidos, deterministas, precisos al fallar—, así que puedes tener cientos sin que la suite duela. El medio es más angosto porque cada test de integración cuesta más —más lento, más setup, un fallo más difícil de ubicar—, así que quieres los suficientes para cubrir las costuras que importan, no uno por cada combinación posible. La cúspide es apenas una punta porque un test de punta a punta —el sistema entero corriendo, con base de datos, red y todo— es lentísimo y frágil, valioso solo para un puñado de recorridos críticos. La forma no es arbitraria: sale de multiplicar "cuánto cuesta cada test" por "cuántos necesito para cubrir su nivel".

Conexión con el módulo: esta lección toma las dos definiciones de la lección 2 y las convierte en una estrategia de cantidades. Es el puente entre "sé qué es cada test" y "sé cuántos escribir y dónde". La lección 4 bajará al detalle de dónde viven las costuras que decides integrar; la 6 clasificará los tipos de integración que pueblan el medio de la pirámide; y la 7 pondrá números al "cuestan más" que justifica por qué el medio es más angosto que la base. Aquí instalamos la forma y su porqué.

Analogía: el control de calidad de una fábrica de bicicletas

Piensa en una fábrica que arma bicicletas. Tiene tres niveles de control de calidad, y gasta en cada uno en proporción inversa a lo que cuesta. Primero, el control de cada pieza: una máquina mide cada rayo, cada balín, cada eslabón de cadena, miles por minuto, por centavos cada medición. Es barato y rapidísimo, así que se miden todas las piezas —es la base—. Segundo, el control de subensamblajes: un operario arma la rueda completa y verifica que gire sin bambolear, o monta el sistema de frenos y comprueba que agarre. Cuesta más —tiempo de una persona, varias piezas juntas—, así que no se prueban todas las combinaciones imaginables, solo los subensamblajes que de verdad importan: la rueda, los frenos, la transmisión. Es el medio. Tercero, la prueba de rodada: alguien se sube a la bicicleta terminada y la maneja por una pista. Es lo más caro y lento —una bici entera, una persona, una pista—, así que solo se ruedan unas pocas de cada lote, las suficientes para confiar en que el modelo entero funciona. Es la punta.

Ninguna fábrica cuerda invierte esta pirámide. Si probaras cada bicicleta con una rodada completa y midieras pocas piezas, gastarías una fortuna en tiempo, atraparías los defectos tardísimo (cuando ya armaste la bici entera) y, cuando una rodada fallara, no sabrías si fue el rayo, la rueda o el freno —tendrías que desarmar todo para averiguarlo—. La pirámide correcta atrapa la mayoría de los defectos en el nivel más barato y más preciso (la pieza), usa el nivel intermedio para las uniones que importan (los subensamblajes), y reserva la prueba cara y ambigua (la rodada) para la confianza final sobre unas pocas unidades. Tu suite de tests es esa fábrica: mide cada pieza a montones, verifica los subensamblajes clave, y rueda el sistema completo solo lo indispensable.

La pirámide, nivel por nivel

Desglosemos los tres niveles con Reservo en mente.

            /\
           /  \      e2e  —  pocos
          /____\      el sistema entero: lentísimo, frágil, ambiguo al fallar
         /      \
        / integr.\   integración  —  algunos
       /__________\   BookingService + SqliteBookingRepository real: cruza la costura
      /            \
     /   unitarios  \  unitarios  —  muchos
    /________________\  BookingService con dobles, price_cents, refund_cents: rápido y preciso

La base: unitarios (muchos). Aquí viven las pruebas de la lógica pura de Reservo (price_cents, refund_cents, overlaps) y las de BookingService con todos sus colaboradores doblados. Son la base ancha porque cada una es casi gratis: corre en microsegundos, no depende de nada externo, y cuando falla apunta a un solo lugar. Puedes cubrir cada ancla, cada borde y cada camino de error sin que la suite se sienta. Es donde debe vivir la mayor parte de tu confianza sobre la lógica.

El medio: integración (algunos). Aquí viven las pruebas que cruzan una costura con una pieza real: BookingService con el SqliteBookingRepository, el repositorio real contra su base de datos, más adelante una llamada HTTP real. Son menos porque cuestan más —abren conexiones, tocan disco, piden setup y limpieza— y porque su trabajo no es re-verificar la lógica (eso ya lo hizo la base), sino verificar las juntas. No necesitas un test de integración por cada regla de negocio; necesitas uno por cada costura con riesgo real de divergencia. Unos pocos, bien elegidos, cubren lo que la base no puede ver.

La cúspide: punta a punta (pocos). Aquí vive el sistema completo corriendo como en producción: la app entera, la base de datos real, la red, quizá un navegador. Son valiosísimos para confirmar que un recorrido crítico funciona de verdad —"un socio reserva Focus y recibe la confirmación"—, pero son lentos (segundos o minutos cada uno), frágiles (fallan por mil razones ajenas a tu código) y ambiguos al fallar (¿qué pieza de la cadena se rompió?). Por eso son una punta: un puñado para los recorridos que no puedes darte el lujo de romper, y nada más. (En esta guía no escribimos e2e con framework web —eso es testing-backend-applications-guide—; te lo nombramos para que el mapa esté completo.)

Ejemplo trabajado: la base de la pirámide, casi gratis

Veamos por qué la base puede ser tan ancha. Aquí está una porción de los unitarios de Reservo: todas las anclas de precio y todas las de reembolso, incluidos los bordes exactos (48 h y 24 h) y los "justo por debajo" (47 h, 23 h). Doce casos, cada uno una verificación de lógica pura, sin un solo doble ni recurso externo.

# tests/test_pure_logic.py — la base: muchos unit tests baratos
from datetime import datetime, timedelta

import pytest

from reservo.models import Booking, Member, Room
from reservo.pricing import price_cents, refund_cents

FOCUS = Room(id="focus", name="Focus", capacity=4, hourly_cents=2500)
PRO = Member(id="m-ana", name="Ana", tier="pro")
BASIC = Member(id="m-leo", name="Leo", tier="basic")


@pytest.mark.parametrize("member, hours, expected", [
    (PRO, 3, 6000),     # 2500*3 = 7500, -20% -> 6000
    (PRO, 1, 2000),
    (PRO, 2, 4000),
    (BASIC, 3, 7500),   # sin descuento
    (BASIC, 1, 2500),
])
def test_price_cents(member, hours, expected):
    assert price_cents(FOCUS, member, hours) == expected


START = datetime(2026, 3, 10, 12)


@pytest.mark.parametrize("hours_before, expected", [
    (72, 6000),   # >= 48h -> 100%
    (48, 6000),   # borde exacto -> 100%
    (47, 3000),   # justo por debajo -> 50%
    (36, 3000),   # [24, 48) -> 50%
    (24, 3000),   # borde exacto -> 50%
    (23, 0),      # justo por debajo -> 0%
    (12, 0),      # < 24h -> 0%
])
def test_refund_cents(hours_before, expected):
    booking = Booking(id="bk-1", room_id="focus", member_id="m-ana",
                      start=START, end=START + timedelta(hours=3),
                      status="confirmed", price_cents=6000)
    now = START - timedelta(hours=hours_before)
    assert refund_cents(booking, 6000, now) == expected

Qué esperar. En mi máquina (Python 3.14.0, pytest 9.1.1):

python3 -m pytest tests/test_pure_logic.py -q
............                                                             [100%]
12 passed in 0.01s

Doce tests en una centésima de segundo. Ese es el secreto de la base ancha: cada uno cuesta tan poco que podrías tener doscientos y la suite seguiría corriendo en un parpadeo. No hay una conexión que abrir, ni un archivo que crear, ni una respuesta de red que esperar; solo aritmética en memoria. Cubrir cada borde de la política de reembolsos —el 48 exacto, el 47 que se le escapa por un pelo, el 24, el 23— no cuesta prácticamente nada, así que se cubre todo. Ahora imagina que cada uno de estos doce, en vez de calcular en memoria, abriera una base de datos real: la lección 7 medirá exactamente cuánto se paga por eso, y verás por qué el nivel de integración no puede ser tan ancho.

Por qué la forma importa: el anti-patrón del cono de helado

La pirámide no es la única forma que una suite puede tomar; es la que funciona. La forma opuesta —la que aparece cuando un equipo no piensa en la proporción— se llama el cono de helado: pocos unitarios en una base flaca, algunos de integración, y una bola enorme de tests de punta a punta arriba, porque "prueban como el usuario de verdad". Suena razonable y es un desastre, por tres razones que salen directo de las propiedades de la lección 2.

Lentitud que te castiga. Una suite dominada por e2e tarda minutos u horas. Una suite lenta se corre poco —nadie espera veinte minutos en cada guardado—, y una suite que se corre poco deja de proteger: los bugs entran entre corrida y corrida. La base ancha de unitarios existe justamente para que la mayor parte de tu confianza venga de tests que corres constantemente.

Fragilidad que te miente. Los tests de punta a punta fallan por razones ajenas a tu código: la red parpadeó, un servicio externo estaba lento, un dato de prueba cambió. Cuando la mayoría de tu suite es frágil, los fallos rojos dejan de significar "hay un bug" y empiezan a significar "vuelve a correrlo a ver si pasa". Ese es el principio del fin: una suite en la que nadie confía es una suite que nadie mira.

Ambigüedad que te cuesta horas. Cuando un e2e falla, el bug puede estar en cualquiera de las diez piezas de la cadena o en cualquiera de sus nueve juntas. Depurarlo es una investigación. Cuando un unit test falla, el bug está en esa unidad. Invertir la pirámide es cambiar cientos de fallos precisos por unos pocos fallos que cuestan una tarde cada uno de rastrear.

La pirámide invertida no atrapa menos bugs en teoría —un e2e ve todo—; atrapa los bugs tarde, caro y borroso, y erosiona la confianza que hace que una suite sirva. La forma correcta empuja cada verificación al nivel más barato que pueda hacerla: la lógica a los unitarios, las juntas a la integración, y solo la confianza final del recorrido completo a la punta.

Errores comunes

Leer la pirámide como una cuota exacta. Qué pasa: alguien toma "muchos, menos, pocos" como "70/20/10 obligatorio" y se angustia por cumplir el porcentaje. Por qué pasa: los números son más fáciles de seguir que el criterio. Cómo detectarlo: si estás borrando tests de integración útiles para "no pasarte del 20%", confundiste la forma con una regla contable. Cómo corregirlo: la pirámide es una forma, no una cuota. La regla real es "empuja cada verificación al nivel más barato que pueda hacerla honestamente". Si eso te da 60/30/10 para tu sistema, perfecto; la proporción exacta la dicta tu arquitectura, no un póster.

Poner en integración lo que es lógica pura. Qué pasa: alguien prueba las siete anclas de refund_cents con el SqliteBookingRepository real, "para que sea más completo". Por qué pasa: la ilusión de que tocar lo real siempre prueba mejor. Cómo detectarlo: si un test de integración verifica una regla que no depende de la costura —la aritmética del reembolso es la misma con cualquier repo—, está en el nivel equivocado. Cómo corregirlo: la lógica va a la base (barata, precisa); la integración se reserva para lo que solo la pieza real puede revelar (la serialización del datetime, no el cálculo del 50%). Bajar una verificación de nivel la hace más rápida y más precisa sin perder nada.

Confiar solo en la base ancha y saltarse el medio. Qué pasa: alguien tiene quinientos unit tests verdes, ninguno de integración, y se siente cubierto. Por qué pasa: la base es cómoda y rápida de escribir; la integración pide setup. Cómo detectarlo: si ninguna prueba toca la pieza real en tus costuras críticas, tu pirámide no tiene medio —es una base sola—, y es exactamente la trampa de la lección 1 (200 unitarios verdes, producción rota). Cómo corregirlo: una pirámide sin su franja de integración no es una pirámide, es una losa que no cubre las juntas. Añade los pocos tests de integración que cubren tus costuras de riesgo; no necesitas muchos, pero necesitas alguno.

Ejercicios

Ejercicio 1 — Asigna cada test a su nivel. Ubica cada uno en la base (unitario), el medio (integración) o la punta (e2e), y justifica: (a) overlaps no deja reservar una sala ya ocupada; (b) book guarda de verdad una fila en la tabla de SQLite y se puede leer; (c) un socio abre la app, reserva Focus por la interfaz web y recibe el correo de confirmación; (d) refund_cents devuelve 3000 a 36 h del inicio.

Ver solución
  • (a) overlaps — base (unitario). Lógica pura de fechas: recibe cuatro instantes, devuelve un booleano. Sin colaboradores, sin costura. Va en la base, barato y preciso.
  • (b) book guarda una fila real en SQLite — medio (integración). Cruza la costura entre BookingService/repositorio y la base de datos real. Verifica la junta, no la lógica. Va en el medio: unos pocos que cubran esta costura bastan.
  • (c) el socio reserva por la web y recibe el correo — punta (e2e). El sistema entero de punta a punta: interfaz, servidor, base de datos, correo. Lento, frágil, ambiguo al fallar. Va en la punta, reservado para el recorrido crítico. (Y en esta guía ni siquiera lo escribimos: es territorio de testing-backend-applications-guide.)
  • (d) refund_cents a 36 h — base (unitario). Aritmética pura de la política de reembolso. Igual que (a): base, barato, uno por cada ancla y borde sin que la suite lo note.

El patrón: la lógica que calcula en memoria va a la base; la junta con una pieza real va al medio; el recorrido completo del sistema va a la punta. Y hay muchos más de (a) y (d) que de (b), y muchos más de (b) que de (c) —eso es la pirámide—.

Ejercicio 2 — Diagnostica el cono de helado. Un equipo tiene 15 unit tests, 10 de integración y 120 de punta a punta. La suite tarda 40 minutos, se corre una vez al día, y la mitad de los fallos rojos se "arreglan" volviéndola a correr. Nombra los tres síntomas del cono de helado que aparecen aquí y qué cambio de forma los aliviaría.

Ver solución

Los tres síntomas están todos presentes:

  1. Lentitud que castiga. 40 minutos por corrida hace que solo se corra una vez al día, así que los bugs entran y viven horas antes de que alguien los vea. Los 120 e2e son la causa: cada uno es lentísimo, y son la mayoría.
  2. Fragilidad que miente. "La mitad de los fallos se arreglan volviendo a correr" es la firma de una suite frágil: los rojos ya no significan "hay un bug" sino "quizás fue la red". La confianza en la suite se erosiona hasta cero.
  3. Ambigüedad que cuesta. Con 120 e2e y solo 15 unitarios, cuando algo falla casi nunca hay un unit test preciso que lo señale; hay que depurar el sistema entero. Cada fallo es una investigación.

El cambio de forma: invertir la proporción hacia una pirámide. Bajar la mayoría de las verificaciones de e2e a unit tests (la lógica que hoy se prueba manejando el sistema entero casi siempre se puede probar aislada, rapidísimo y preciso), dejar una franja de integración para las costuras reales, y quedarse con un puñado de e2e solo para los recorridos críticos. La suite pasaría de 40 minutos a segundos, los rojos volverían a significar algo, y los fallos volverían a apuntar a un lugar.

Ejercicio 3 — ¿Cuántos de integración para Reservo? Reservo tiene tres costuras reales: BookingServiceSqliteBookingRepository (persistencia), el repositorio ↔ su base de datos (transacciones), y más adelante un límite HTTP. Un compañero propone: "escribamos un test de integración por cada regla de negocio: uno para cada ancla de precio, uno para cada ancla de reembolso, con el repo real". ¿Es una buena idea? Explica desde la pirámide.

Ver solución

No es una buena idea: infla el medio de la pirámide con tests que pertenecen a la base. Las anclas de precio (6000, 7500, 2000...) y de reembolso (6000, 3000, 0) son lógica pura: price_cents y refund_cents calculan lo mismo sin importar qué repositorio haya detrás. Probarlas con el SqliteBookingRepository real no verifica ninguna junta nueva —el cálculo del 20% de descuento no cruza la costura de la base de datos—; solo hace cada test cientos de veces más lento y más frágil, a cambio de cero información adicional. Es la lógica en el nivel equivocado.

Lo correcto: las anclas van a la base, como unit tests parametrizados (justo lo del ejemplo trabajado, 12 en 0.01s). En el medio van los pocos tests de integración que cubren lo que solo la costura revela: que una reserva guardada y recuperada del repo real conserva sus campos (incluida la trampa del datetime), que get de un id ausente lanza contra la base de datos real, que una transacción hace rollback. Uno o dos por costura, no uno por regla de negocio. La pirámide se mantiene ancha abajo y angosta en el medio precisamente evitando esta duplicación: cada verificación en el nivel más barato que pueda hacerla.

Resumen y siguiente paso

En esta lección convertiste las dos definiciones de la lección 2 en una estrategia de cantidades. La pirámide de tests —muchos unitarios, menos de integración, pocos de punta a punta— no es un adorno: es lo que sale de multiplicar el costo de cada test por cuántos necesitas de cada nivel. La base es ancha porque los unitarios son casi gratis (viste doce en 0.01s); el medio es angosto porque la integración cuesta más y solo se necesita para las costuras que importan; la punta es mínima porque el e2e es lento, frágil y ambiguo. Y viste el anti-patrón: el cono de helado invertido, que castiga con lentitud, miente con fragilidad y cobra con ambigüedad.

Antes de avanzar deberías poder: dibujar la pirámide y decir qué va en cada nivel para Reservo; explicar por qué esa forma, y no la invertida, mantiene una suite útil; y reconocer cuándo una verificación está en el nivel equivocado (lógica en integración, o una costura sin ningún test de integración).

La pirámide te dijo que quieres algunos tests de integración, para las costuras que importan. La pregunta natural es: ¿dónde están esas costuras, exactamente? ¿Cómo las reconoces en el código, y cómo decides en cuál doblar y en cuál integrar? Eso es la lección 4: las costuras, los puntos donde dos componentes se conectan.

Recursos