Módulo 3: Integration y E2E Tests
La Test Pyramid: Estrategia y Trade-offs
La Test Pyramid: Estrategia y Trade-offs
Descripción de la cápsula
Tienes tiempo limitado. Tienes un presupuesto de tests. ¿Qué tests escribes primero? ¿Y cuántos de cada tipo? La test pyramid no es una regla dogmática — es un framework de decisión que te ayuda a maximizar confianza minimizando costo. En esta cápsula aprenderás la estrategia detrás de los tres niveles, los trade-offs de cada uno, y cómo usar Claude Code para generar tests en el nivel correcto con el prompt correcto.
Sin entender la pyramid, tiendes a hacer una de dos cosas: escribir solo unit tests (rápidos pero que no detectan fallos de integración) o escribir demasiados E2E tests (lentos, frágiles, costosos de mantener). La pyramid te da criterio para equilibrar velocidad, confianza y costo.
Al final, tendrás una estrategia clara: qué testear en cada nivel, cuántos tests en cada uno, y los prompts exactos para que Claude Code genere tests en el nivel apropiado.
La Test Pyramid Explicada
Los tres niveles
La test pyramid divide los tests en tres categorías según qué verifican y cuánto cuestan:
/\
/ \
/ E2E \ ← Pocos, lentos, alta confianza
/──────\
/ \
/Integration\ ← Medios, velocidad media
/────────────\
/ \
/ Unit \ ← Muchos, rápidos, aislados
/──────────────────\
Unit tests (base):
- Velocidad: milisegundos por test
- Alcance: funciones o clases aisladas, sin dependencias externas
- Lo que prueban: lógica pura, cálculos, validaciones, transformaciones
- Costo de mantenimiento: muy bajo
Integration tests (medio):
- Velocidad: segundos por test
- Alcance: interacción entre componentes (endpoints HTTP, base de datos, servicios)
- Lo que prueban: que las piezas encajan correctamente — el endpoint recibe el request y la DB guarda los datos
- Costo de mantenimiento: medio
E2E tests (punta):
- Velocidad: segundos a minutos
- Alcance: flujo completo de usuario de principio a fin
- Lo que prueban: que el sistema funciona como un todo — registrar → login → crear recurso → leer → actualizar → eliminar
- Costo de mantenimiento: alto
Representación visual con proporciones
Proporción típica recomendada: 70% unit, 20% integration, 10% E2E.
Cantidad de tests (aproximada)
Unit: ████████████████████████████████████████ ~70%
Integration: ████████████ ~20%
E2E: ██████ ~10%
Tiempo de ejecución (invertido - unit es rápido)
Unit: ██ <1 min total
Integration: ████████ ~1-5 min
E2E: ████████████████████████████ 5-30 min
La base es ancha porque los unit tests son baratos de escribir y ejecutar. La punta es estrecha porque los E2E tests son caros. Invertir en muchos unit tests te da feedback rápido sin sacrificar velocidad del ciclo de desarrollo.
Trade-offs en Cada Nivel
| Criterio | Unit | Integration | E2E |
|---|---|---|---|
| Velocidad | Milisegundos | Segundos | Segundos-Minutos |
| Confianza | Solo lógica | Interfaces entre componentes | Sistema completo |
| Costo de mantenimiento | Bajo | Medio | Alto |
| Flakiness | Muy bajo | Bajo | Alto |
| Complejidad de setup | Ninguna | Alguna (DB, TestClient) | Significativa |
Interpretación práctica
- ✅ Unit: Si un unit test falla, sabes que la lógica de la función está mal. Pero no sabes si el endpoint expone esa lógica correctamente.
- ✅ Integration: Si un integration test falla, sabes que hay un problema en la interfaz (endpoint ↔ DB, endpoint ↔ servicio). No necesariamente en la lógica interna.
- ✅ E2E: Si un E2E test falla, sabes que algo está roto en el flujo completo. Pero no sabes dónde — podría ser el frontend, la API, la DB, o la red.
Flakiness = tests que a veces pasan y a veces fallan sin que cambies el código. Los E2E tests son flaky porque dependen de muchos componentes — timeout de red, orden de ejecución, estado de la DB. Los unit tests casi nunca son flaky porque no hay dependencias externas.
Velocidad en la práctica
En un proyecto típico con 100 unit tests, 30 integration y 10 E2E:
Unit (100 tests): ~3-5 segundos total
Integration (30): ~15-30 segundos
E2E (10): ~2-5 minutos
Total: ~3-6 minutos para la suite completa
Si inviertes la proporción (10 unit, 30 integration, 100 E2E), la suite podría tardar 30-60 minutos. El feedback loop se rompe — nadie corre los tests antes de commit.
Cuántos Tests en Cada Nivel
Regla de oro: 70% / 20% / 10%
- ~70% unit: Lógica de negocio, validaciones, cálculos, transformaciones
- ~20% integration: Endpoints, queries a DB, llamadas entre servicios
- ~10% E2E: Flujos críticos de usuario (login, CRUD completo, checkout)
Depende del proyecto
| Tipo de proyecto | Unit | Integration | E2E |
|---|---|---|---|
| API-heavy (CRUD REST) | 60% | 30% | 10% |
| Lógica compleja (calculadora, engine) | 80% | 15% | 5% |
| Flujos críticos (pagos, auth) | 65% | 20% | 15% |
| Simple, pocos endpoints | 70% | 20% | 10% |
Si tu proyecto es una API con muchos endpoints y poca lógica de negocio compleja, tendrás más integration tests. Si es un engine de cálculos con pocos endpoints, tendrás más unit tests.
El anti-patrón "Ice Cream Cone"
/\
/ \
/ \ ← Demasiados E2E
/ E2E \
/────────\
/ \
/ Unit \ ← Muy pocos unit
/──────────────\
Síntomas:
- Suite tarda 30+ minutos en correr
- Tests fallan aleatoriamente (flaky)
- Cualquier cambio rompe decenas de tests
- Nadie quiere correr los tests antes de commit
Solución: Reducir E2E, aumentar unit e integration. Los unit tests dan feedback rápido y no son flaky. Los E2E deben reservarse para flujos realmente críticos.
Qué Testear en Cada Nivel
Unit: Lógica aislada
✅ Funciones puras (input → output)
✅ Cálculos (descuentos, totales, promedios)
✅ Validaciones (email, formato, rangos)
✅ Transformaciones de datos (parse, format)
✅ Reglas de negocio que no tocan I/O
❌ Llamadas HTTP
❌ Acceso a base de datos
❌ Sistema de archivos
❌ Servicios externos
Integration: Interacciones entre componentes
✅ Endpoints HTTP (status codes, response body, headers)
✅ Queries a DB (CRUD, relaciones)
✅ Interacción servicio ↔ repositorio
✅ Serialización/deserialización (request → model → DB)
✅ Autenticación a nivel de endpoint
❌ Flujo completo de múltiples endpoints
❌ UI o browser
❌ Múltiples servicios orquestados (eso es E2E)
E2E: Flujos completos
✅ Registrar usuario → login → crear recurso → leer → actualizar → eliminar
✅ Flujo de checkout completo
✅ Autenticación → operación protegida
✅ Casos críticos de negocio de principio a fin
❌ Cada variación de cada endpoint (eso es integration)
❌ Edge cases de lógica (eso es unit)
Prompts de Claude Code por Nivel
El nivel del test se define en el prompt. Si pides "tests" sin especificar, Claude Code tenderá a unit tests por defecto.
Unit tests
Genera unit tests para la función [nombre] en [archivo].
La función [describe brevemente qué hace].
Cubre:
- Happy path
- Edge cases (vacío, None, límites)
- Error handling (inputs inválidos)
Usa pytest, un assert por test, nombres descriptivos.
Ejemplo concreto:
Genera unit tests para la función validate_todo_title en todos.py.
La función valida que el título tenga entre 1 y 200 caracteres y no sea solo espacios.
Cubre: título válido, vacío, muy largo, solo espacios, None.
Usa pytest, un assert por test.
Integration tests
Genera integration tests para el endpoint [método] [ruta] de la API FastAPI.
Usa TestClient de FastAPI. Verifica:
- Status code correcto
- Estructura del response body
- Persistencia en DB cuando aplica
Cubre: success case, validation errors, not found.
Ejemplo concreto:
Genera integration tests para el endpoint POST /todos de la API.
Usa TestClient. Verifica que retorne 201 con el todo creado en el body,
y que el todo exista en la base de datos.
Cubre: todo válido, título vacío (422), título muy largo (422).
E2E tests
Genera un E2E test para el flujo completo: crear todo → obtenerlo → actualizarlo → eliminarlo.
Usa httpx o TestClient. El test debe:
1. Crear un todo via POST
2. Obtenerlo via GET con el id
3. Actualizarlo via PUT
4. Eliminarlo via DELETE
5. Verificar que GET ya no lo encuentra (404)
Un solo test que valide el ciclo de vida completo.
Ejemplo concreto:
Genera un E2E test que valide el flujo CRUD completo de todos.
Pasos: POST crear todo → GET por id → PUT actualizar → DELETE → GET verificar 404.
Un solo test, flujo completo.
Diferencias clave
| Nivel | El prompt especifica | Claude Code genera |
|---|---|---|
| Unit | "unit tests para esta función" | Tests de función aislada, sin HTTP ni DB |
| Integration | "integration tests para este endpoint" | TestClient, assertions de status y body |
| E2E | "E2E test para este flujo" | Secuencia de requests, flujo completo |
Errores comunes al pedir tests a Claude Code:
- Pedir "tests" sin especificar nivel → suele generar unit tests por defecto
- Dar contexto de FastAPI y pedir "tests" → puede generar integration sin que lo quieras
- Pedir "un test" para un flujo → puede generar un solo test que mezcla unit+integration; para E2E debes decir "flujo completo" o "ciclo de vida"
Ejemplo Real: Todo App en los 3 Niveles
Código de la aplicación
# main.py (FastAPI app)
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field
app = FastAPI()
# In-memory store para simplificar (en proyecto real sería DB)
todos_db: dict[int, dict] = {}
_id_counter = 0
class TodoCreate(BaseModel):
title: str = Field(..., min_length=1, max_length=200)
class TodoUpdate(BaseModel):
title: str | None = Field(None, min_length=1, max_length=200)
class TodoResponse(BaseModel):
id: int
title: str
def validate_todo_title(title: str) -> str:
"""Pure validation logic — tested at unit level."""
if not title or not title.strip():
raise ValueError("Title cannot be empty")
if len(title) > 200:
raise ValueError("Title must be at most 200 characters")
return title.strip()
@app.post("/todos", response_model=TodoResponse)
def create_todo(todo: TodoCreate):
global _id_counter
validated = validate_todo_title(todo.title)
_id_counter += 1
todo_obj = {"id": _id_counter, "title": validated}
todos_db[_id_counter] = todo_obj
return todo_obj
@app.get("/todos/{todo_id}", response_model=TodoResponse)
def get_todo(todo_id: int):
if todo_id not in todos_db:
raise HTTPException(404, "Todo not found")
return todos_db[todo_id]
@app.put("/todos/{todo_id}", response_model=TodoResponse)
def update_todo(todo_id: int, todo: TodoUpdate):
if todo_id not in todos_db:
raise HTTPException(404, "Todo not found")
if todo.title is not None:
todos_db[todo_id]["title"] = validate_todo_title(todo.title)
return todos_db[todo_id]
@app.delete("/todos/{todo_id}", status_code=204)
def delete_todo(todo_id: int):
if todo_id not in todos_db:
raise HTTPException(404, "Todo not found")
del todos_db[todo_id]
Unit: test_create_todo_validates_title
# tests/unit/test_todo_validation.py
import pytest
from main import validate_todo_title
def test_valid_title_returns_stripped():
assert validate_todo_title(" Buy milk ") == "Buy milk"
def test_empty_string_raises():
with pytest.raises(ValueError, match="cannot be empty"):
validate_todo_title("")
def test_whitespace_only_raises():
with pytest.raises(ValueError, match="cannot be empty"):
validate_todo_title(" \t\n ")
def test_title_too_long_raises():
with pytest.raises(ValueError, match="at most 200"):
validate_todo_title("a" * 201)
def test_title_exactly_200_chars_ok():
result = validate_todo_title("a" * 200)
assert len(result) == 200
Rápido, aislado, sin levantar la API. Prueba solo la lógica de validación.
Integration: test_post_todos_returns_201
# tests/integration/test_todos_api.py
from fastapi.testclient import TestClient
from main import app, todos_db
client = TestClient(app)
def setup_function():
todos_db.clear()
def test_post_todos_returns_201():
response = client.post("/todos", json={"title": "Learn pytest"})
assert response.status_code == 201
data = response.json()
assert "id" in data
assert data["title"] == "Learn pytest"
assert data["id"] in todos_db
def test_post_todos_empty_title_returns_422():
response = client.post("/todos", json={"title": ""})
assert response.status_code == 422
def test_get_todo_returns_200():
create_resp = client.post("/todos", json={"title": "Test todo"})
todo_id = create_resp.json()["id"]
response = client.get(f"/todos/{todo_id}")
assert response.status_code == 200
assert response.json()["title"] == "Test todo"
def test_get_todo_not_found_returns_404():
response = client.get("/todos/99999")
assert response.status_code == 404
Usa TestClient. Verifica status codes, response body, y que los datos persistieron en todos_db. No prueba el flujo completo de múltiples requests encadenados.
E2E: test_full_todo_lifecycle_create_read_update_delete
# tests/e2e/test_todo_lifecycle.py
from fastapi.testclient import TestClient
from main import app, todos_db
client = TestClient(app)
def setup_function():
todos_db.clear()
def test_full_todo_lifecycle_create_read_update_delete():
# 1. Create
create_resp = client.post("/todos", json={"title": "E2E todo"})
assert create_resp.status_code == 201
todo_id = create_resp.json()["id"]
# 2. Read
get_resp = client.get(f"/todos/{todo_id}")
assert get_resp.status_code == 200
assert get_resp.json()["title"] == "E2E todo"
# 3. Update
update_resp = client.put(f"/todos/{todo_id}", json={"title": "E2E todo updated"})
assert update_resp.status_code == 200
assert update_resp.json()["title"] == "E2E todo updated"
# 4. Delete
delete_resp = client.delete(f"/todos/{todo_id}")
assert delete_resp.status_code == 204
# 5. Verify gone
get_after_resp = client.get(f"/todos/{todo_id}")
assert get_after_resp.status_code == 404
Un solo test que valida el ciclo de vida completo. Si este pasa, sabes que el flujo CRUD funciona de punta a punta.
Resumen del ejemplo: misma feature, tres enfoques
| Nivel | Qué verifica | Dependencias |
|---|---|---|
| Unit | validate_todo_title rechaza vacío, muy largo, acepta 200 chars | Ninguna (función pura) |
| Integration | POST retorna 201, body correcto, dato en DB; GET 404 para id inexistente | FastAPI app, TestClient, todos_db |
| E2E | Crear → leer → actualizar → eliminar → 404 | Toda la app, flujo completo |
Si cambias la validación de título (ej. max 100 chars), el unit test falla primero. Si rompes el endpoint POST (ej. typo en el path), el integration test falla. Si rompes algo en el flujo (ej. delete no elimina de verdad), el E2E falla.
Estructura de carpetas recomendada
Para mantener la pyramid clara en el código:
project/
├── src/
│ └── main.py
├── tests/
│ ├── unit/
│ │ ├── test_validation.py
│ │ └── test_utils.py
│ ├── integration/
│ │ ├── test_todos_api.py
│ │ └── test_auth_endpoints.py
│ ├── e2e/
│ │ └── test_todo_lifecycle.py
│ └── conftest.py # Fixtures compartidas
Pytest puede ejecutar por nivel:
pytest tests/unit/ # Solo unit (desarrollo rápido)
pytest tests/integration/ # Unit + integration (pre-commit)
pytest tests/ # Suite completa (CI)
Tip: Durante TDD, corre solo los unit tests — feedback en segundos. Antes de push, corre unit + integration. Los E2E solo en CI o cuando cambies flujos críticos.
Tabla Comparativa: Cuándo Usar Cada Nivel
| Situación | Usa |
|---|---|
Verificar que calculate_discount(100, 10) retorna 90 | Unit |
| Verificar que una función valida correctamente el email | Unit |
Verificar que POST /todos retorna 201 y guarda en DB | Integration |
Verificar que GET /todos/1 retorna 404 si no existe | Integration |
| Verificar que el flujo registro→login→crear recurso funciona | E2E |
| Verificar que un cambio en el endpoint rompe las respuestas | Integration |
| Verificar que un cambio en la lógica de validación rompe el endpoint | Unit + Integration |
| Detectar regresiones en flujos críticos antes de deploy | E2E |
| Feedback rápido durante desarrollo (TDD) | Unit |
| Validar que la API cumple el contrato esperado | Integration |
Ejercicios
Ejercicio 1: Clasificar tests (Fácil)
Clasifica cada test como Unit, Integration o E2E:
test_calculate_total_with_tax_returns_correct_amount()test_post_users_returns_201_with_user_in_db()test_login_then_create_post_then_delete_post()test_validate_password_rejects_short_password()test_get_product_returns_404_when_not_found()
Ver solución
- Unit — Prueba una función de cálculo aislada.
- Integration — Prueba que el endpoint POST persiste en DB.
- E2E — Flujo completo: login → crear → eliminar.
- Unit — Prueba lógica de validación.
- Integration — Prueba el endpoint GET y su respuesta 404.
Ejercicio 2: Elegir el nivel correcto (Medio)
Tienes una API de productos. ¿Qué nivel usarías para verificar cada uno de estos comportamientos?
- a) El precio con IVA se calcula como
precio * 1.21 - b) Al hacer
POST /products, el producto aparece enGET /products/{id} - c) Un usuario puede registrarse, hacer login, crear un producto y verlo en su lista
Ver solución
- a) Unit — Cálculo puro, sin HTTP ni DB.
- b) Integration — Verifica que el endpoint POST persiste y que GET lo recupera. Interacción endpoint ↔ storage.
- c) E2E — Flujo completo de usuario: register → login → create → read. Múltiples endpoints encadenados.
Ejercicio 3: Detectar el anti-patrón (Medio)
Un equipo tiene 150 tests: 20 unit, 30 integration, 100 E2E. La suite tarda 45 minutos. Identifica el problema y propón una redistribución.
Ver solución
Problema: Ice cream cone — demasiados E2E, muy pocos unit. La suite es lenta y probablemente flaky.
Propuesta de redistribución (ejemplo):
- Unit: 100+ (lógica de negocio, validaciones, cálculos)
- Integration: 40 (endpoints principales, CRUD)
- E2E: 10 (flujos críticos: auth, checkout, CRUD completo)
Resultado esperado: Suite en 5-15 min, menos flakiness, feedback más rápido.
Ejercicio 4: Escribir prompts para Claude Code (Medio)
Tienes una función format_price(amount: float, currency: str) -> str y un endpoint GET /products/{id}. Escribe los prompts que usarías para pedir a Claude Code:
- Unit tests para
format_price - Integration tests para
GET /products/{id}
Ver solución
1. Unit tests para format_price:
Genera unit tests para la función format_price(amount: float, currency: str) -> str.
La función formatea el monto según la moneda (EUR → "10,50 €", USD → "$10.50").
Cubre: montos válidos, cero, negativos, currency desconocida.
Usa pytest, nombres descriptivos.
2. Integration tests para GET /products/{id}:
Genera integration tests para el endpoint GET /products/{id} de una API FastAPI.
Usa TestClient. Verifica:
- 200 cuando el producto existe, body con id, name, price
- 404 cuando no existe
Cubre ambos casos.
Ejercicio 5: Implementar los 3 niveles para una función (Difícil)
Tienes def add(a: int, b: int) -> int en un endpoint POST /calc que recibe {"a": 1, "b": 2} y retorna {"result": 3}. Escribe:
- Un unit test para
add - Un integration test para
POST /calc - Un E2E test que use el resultado de un POST en una operación subsiguiente (ej. POST calc, luego GET algo que use ese resultado — si no aplica, diseña un mini-flujo)
Para el E2E, si la API no tiene flujo encadenado, inventa uno simple: por ejemplo, POST /calc guarda el último resultado y GET /calc/last lo retorna. E2E: POST → GET /calc/last.
Ver solución
1. Unit:
# tests/unit/test_add.py
import pytest
from main import add
def test_add_positive_numbers():
assert add(2, 3) == 5
def test_add_with_zero():
assert add(0, 5) == 5
def test_add_negative():
assert add(-1, 1) == 0
2. Integration:
# tests/integration/test_calc_api.py
from fastapi.testclient import TestClient
from main import app
client = TestClient(app)
def test_post_calc_returns_200_with_result():
response = client.post("/calc", json={"a": 1, "b": 2})
assert response.status_code == 200
assert response.json()["result"] == 3
3. E2E (asumiendo GET /calc/last):
# tests/e2e/test_calc_flow.py
from fastapi.testclient import TestClient
from main import app
client = TestClient(app)
def test_calc_then_get_last():
client.post("/calc", json={"a": 10, "b": 20})
response = client.get("/calc/last")
assert response.status_code == 200
assert response.json()["result"] == 30
Si no existe GET /calc/last, el E2E podría ser: dos POST secuenciales y verificar que la API mantiene estado consistente (según diseño). El punto es que E2E valida un flujo de múltiples requests.
Ejercicio 6: Detectar qué nivel falló (Medio)
Tienes una API de pedidos. Un bug hace que el total con descuento se calcule mal. Tienes tests en los 3 niveles. ¿Qué test fallaría primero si el bug está en: a) la función calculate_discounted_total, b) el endpoint que no recibe bien el discount_percent del body, c) el flujo completo que no pasa el cupón entre pasos?
Ver solución
- a) Unit — Si el bug está en
calculate_discounted_total, el unit test que llama a esa función falla. Es el primer nivel que la ejecuta directamente. - b) Integration — Si la función está bien pero el endpoint no parsea/recibe correctamente el
discount_percent, el integration test del endpoint falla. El unit test de la función pasaría. - c) E2E — Si el bug está en que el cupón no se propaga entre pasos del flujo (ej. crear carrito → aplicar cupón → checkout), solo el E2E que recorre todo el flujo lo detecta. Unit e integration de cada pieza podrían pasar.
Ejercicio 7: Diseñar la pyramid para tu proyecto (Medio)
Imagina una API REST de tareas (tasks) con: crear, listar, obtener por id, actualizar, eliminar. Hay validación de título (1-200 chars) y un campo completed: bool. Estima cuántos tests de cada tipo tendrías y qué cubrirían.
Ver solución
Estimación ejemplo:
| Nivel | Cantidad | Qué cubren |
|---|---|---|
| Unit | ~12 | validate_title (5 casos), format_task_for_response (2-3), lógica de filtro completed (2-3) |
| Integration | ~15 | POST (201, 422), GET list (200 vacío, 200 con items), GET by id (200, 404), PUT (200, 404, 422), DELETE (204, 404) |
| E2E | ~2 | Flujo CRUD completo; flujo crear varias → filtrar por completed |
Total: ~29 tests. Proporción aproximada 40% unit, 50% integration, 10% E2E. Para una API CRUD es razonable tener más integration que unit si la lógica de negocio es poca.
Conexión con Proyecto
El proyecto de este módulo consiste en construir una test pyramid completa para una API REST CRUD de items. Esta cápsula te da la estrategia:
- Unit: Validaciones, formateo, reglas de negocio de items.
- Integration: Cada endpoint (GET, POST, PUT, DELETE) con TestClient, verificando status y body.
- E2E: Al menos un flujo completo: crear item → leerlo → actualizarlo → eliminarlo → verificar 404.
En la cápsula 03 usarás FastAPI TestClient para implementar los integration tests. La pyramid que diseñas aquí (qué testear en cada nivel) se materializa en las próximas cápsulas.
Troubleshooting
1. "¿Por qué no escribir solo E2E? Dan más confianza."
Causa: Confundir confianza con eficiencia. Los E2E dan confianza en el flujo completo, pero son lentos, flaky y difíciles de mantener. Si todo es E2E, el ciclo de feedback se vuelve insostenible.
Solución: Usa E2E para los flujos que realmente importan (3-5 por proyecto típico). El resto de la confianza viene de unit + integration, que son rápidos y estables.
2. "Mis unit tests pasan pero el endpoint falla en producción"
Causa: Los unit tests verifican la lógica aislada. No verifican serialización, routing, middleware, ni la integración con la DB real.
Solución: Añade integration tests. Si validate_todo_title funciona en unit pero el endpoint retorna 500, el problema está en la integración (Pydantic, endpoint, DB). Los integration tests lo detectan.
3. "Los E2E tests son flaky — a veces pasan, a veces no"
Causa: Dependencias externas: timeouts, orden de ejecución, estado compartido entre tests, DB no limpia.
Solución: Asegura que cada E2E tenga setup/teardown que limpie el estado. Usa bases de datos en memoria o containers efímeros. Evita dependencias de tiempo (usa mocks para datetime si aplica). Si un E2E sigue siendo flaky, considera bajarlo a integration.
4. "No sé si un test debe ser unit o integration"
Causa: Criterio poco claro. La pregunta clave: ¿toca I/O (HTTP, DB, archivos)?
Solución: Si el test no hace requests HTTP ni accede a DB → unit. Si hace requests (TestClient) o usa DB real/test DB → integration. Si encadena múltiples requests en un flujo de usuario → E2E.
5. "Claude Code genera integration tests cuando pido unit tests"
Causa: El prompt no especifica el nivel con suficiente claridad, o el contexto (archivo con FastAPI app) hace que Claude Code asuma integration.
Solución: Sé explícito: "Genera unit tests para la función X. No uses TestClient ni hagas requests HTTP. Prueba solo la función aislada." Incluye la firma de la función y qué debe hacer.
Resumen
- ✅ La test pyramid tiene tres niveles: unit (base), integration (medio), E2E (punta)
- ✅ Trade-offs: unit es rápido y barato; integration da confianza en interfaces; E2E valida el sistema completo pero es lento y frágil
- ✅ Proporción típica: ~70% unit, ~20% integration, ~10% E2E (varía según proyecto)
- ✅ Evita el ice cream cone: demasiados E2E y pocos unit
- ✅ Unit: lógica pura, validaciones, cálculos. Integration: endpoints, DB. E2E: flujos completos
- ✅ El prompt define el nivel: "unit tests para esta función" vs "integration tests para este endpoint" vs "E2E para este flujo"
- ✅ Una misma feature se testea de forma distinta en cada nivel; el ejemplo de la todo app muestra los tres enfoques
Próxima cápsula: API testing con FastAPI TestClient — implementar integration tests con GET, POST, PUT, DELETE.
Recursos Adicionales
- Martin Fowler: Test Pyramid — Origen del concepto
- Ham Vocke: The Practical Test Pyramid — Guía práctica detallada con ejemplos
- FastAPI: Testing — Documentación oficial de TestClient
- Kent C. Dodds: Testing Trophy — Visión alternativa (testing trophy) y clasificaciones
- Google: Testing Blog — Just Say No to More End-to-End Tests — Por qué no abusar de E2E
- pytest: Organizing test directory — Estructura de carpetas para unit/integration/e2e
Módulo 3, Cápsula 02 — Testing with Claude Code Guide La pyramid es estrategia, no dogma: equilibra velocidad, confianza y costo