Módulo 3: Integration y E2E Tests
E2E Testing: Flujos Completos de API
E2E Testing: Flujos Completos de API
Descripción de la cápsula
En las cápsulas anteriores testearon endpoints uno por uno: POST retorna 201, GET retorna 404 cuando no existe. Esos son integration tests — verifican interfaces. Pero un cliente real de tu API no llama a un solo endpoint. Llama a varios en secuencia: crear recurso → obtenerlo → actualizarlo → eliminarlo. Si el flujo completo falla en algún eslabón, los integration tests individuales pueden estar verdes y el usuario tener una experiencia rota.
Esta cápsula cubre E2E testing para APIs: validar flujos completos tal como los usaría un cliente HTTP real. No hablamos de Selenium ni Playwright — aquí E2E significa backend end-to-end: la cadena completa request → routing → handler → lógica → base de datos → response, pero atravesando múltiples requests que forman una historia de usuario. Al final podrás escribir E2E tests con TestClient, organizarlos correctamente, y evitar la trampa del flakiness.
Qué Significa E2E para APIs
Testing del flujo completo como cliente real
Un E2E test de API simula lo que hace un consumidor de tu API cuando ejecuta una operación completa:
Cliente real (Postman, frontend, otra API):
1. POST /users → Crea usuario
2. POST /login → Obtiene token
3. POST /orders → Crea orden (con token)
4. GET /orders/1 → Lee la orden
5. PATCH /orders/1 → Actualiza estado
6. GET /orders/1 → Verifica el cambio
Un integration test verifica solo uno de esos pasos aislado: "POST /orders retorna 201 con el body correcto". Un E2E test verifica que toda la secuencia funciona de principio a fin. Si en el paso 4 la orden no existe (por un bug en el paso 3), el integration de POST podría estar verde y el flujo real romperse.
La cadena completa: request → response
En un E2E test de API verificas:
- Request HTTP llega correctamente al servidor
- Routing dirige a la función correcta
- Handler procesa el request
- Lógica de negocio se ejecuta
- Base de datos persiste o lee datos
- Response vuelve con el formato esperado
Y lo haces para varios requests encadenados. Cada request puede depender del resultado del anterior (por ejemplo, el id devuelto en un POST se usa en el GET siguiente).
No es testing de browser
En esta guía, E2E = flujos de API, no testing de UI. No usas Selenium, Playwright ni headless browsers. El cliente es TestClient de FastAPI o httpx — pura HTTP contra tu backend. El frontend no interviene.
E2E vs Integration: La Diferencia Clave
Integration: un endpoint en aislamiento
Integration test típico:
- POST /items con json válido → assert 201
- Verificar que el body tiene id, name, price
- (Opcional) Verificar que existe en DB
Un solo request. Un solo endpoint. El test pasa o falla según ese punto de contacto.
E2E: un flujo de múltiples endpoints
E2E test típico:
1. POST /items → 201, guardar id
2. GET /items/{id} → 200, mismo name
3. PUT /items/{id} → 200, name actualizado
4. DELETE /items/{id} → 200
5. GET /items/{id} → 404 (recurso eliminado)
Varios requests en secuencia. Cada uno depende del anterior. Si alguno falla, el test falla — y sabes que hay un problema en el flujo completo, no solo en un endpoint.
Tabla comparativa
| Aspecto | Integration | E2E |
|---|---|---|
| Requests por test | 1 (a veces 2 para setup) | 3-10+ |
| Qué verifica | La interfaz del endpoint | El workflow de usuario |
| Velocidad | Rápido | Más lento |
| Confianza | Endpoint funciona | Flujo completo funciona |
| Mantenimiento | Bajo | Mayor |
| Flakiness | Bajo | Alto si no cuidas el setup |
Regla práctica
- Integration verifica que las piezas encajan (endpoint ↔ DB, endpoint ↔ servicio).
- E2E verifica que el flujo de negocio funciona de punta a punta.
Escribir E2E Tests con TestClient
Setup mínimo
Necesitas la app FastAPI y un cliente. Si usas base de datos, cada test debe partir de un estado limpio (o usar transacciones/DB en memoria).
# conftest.py (o en el archivo de tests)
import pytest
from fastapi.testclient import TestClient
from main import app
@pytest.fixture
def client():
return TestClient(app)
Ejemplo: ciclo de vida CRUD completo
# tests/e2e/test_item_lifecycle.py
import pytest
from fastapi.testclient import TestClient
from main import app, items_db # asumiendo store en memoria o DB de test
client = TestClient(app)
def setup_function():
"""Limpia el estado antes de cada test (evita flakiness)."""
items_db.clear()
class TestItemLifecycle:
"""E2E: Ciclo de vida CRUD completo para items."""
def test_full_item_crud_lifecycle(self):
# CREATE
create_response = client.post(
"/items",
json={"name": "E2E Item", "price": 42.0}
)
assert create_response.status_code == 201
data = create_response.json()
item_id = data["id"]
# READ
get_response = client.get(f"/items/{item_id}")
assert get_response.status_code == 200
assert get_response.json()["name"] == "E2E Item"
# UPDATE
update_response = client.put(
f"/items/{item_id}",
json={"name": "Updated", "price": 99.0}
)
assert update_response.status_code == 200
assert update_response.json()["name"] == "Updated"
# DELETE
delete_response = client.delete(f"/items/{item_id}")
assert delete_response.status_code == 200 # o 204 según tu API
# VERIFY DELETED
verify_response = client.get(f"/items/{item_id}")
assert verify_response.status_code == 404
Un solo test que valida el flujo completo. Si falla, sabes que algo en la cadena CRUD está roto.
Código de la app de ejemplo (completo)
# main.py — API mínima para que los tests funcionen
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
app = FastAPI()
items_db: dict[int, dict] = {}
_id_counter = 0
class ItemCreate(BaseModel):
name: str
price: float
class ItemUpdate(BaseModel):
name: str | None = None
price: float | None = None
class ItemResponse(BaseModel):
id: int
name: str
price: float
@app.post("/items", response_model=ItemResponse)
def create_item(item: ItemCreate):
global _id_counter
_id_counter += 1
obj = {"id": _id_counter, "name": item.name, "price": item.price}
items_db[_id_counter] = obj
return obj
@app.get("/items/{item_id}", response_model=ItemResponse)
def get_item(item_id: int):
if item_id not in items_db:
raise HTTPException(404, "Item not found")
return items_db[item_id]
@app.put("/items/{item_id}", response_model=ItemResponse)
def update_item(item_id: int, item: ItemUpdate):
if item_id not in items_db:
raise HTTPException(404, "Item not found")
obj = items_db[item_id]
if item.name is not None:
obj["name"] = item.name
if item.price is not None:
obj["price"] = item.price
return obj
@app.delete("/items/{item_id}")
def delete_item(item_id: int):
if item_id not in items_db:
raise HTTPException(404, "Item not found")
del items_db[item_id]
return {"status": "deleted"}
Patrones E2E Comunes
1. Registro → login → recurso protegido
def test_user_registration_login_and_protected_access(self, client):
# 1. Registrar
reg = client.post("/auth/register", json={
"email": "e2e@test.com",
"password": "secure123"
})
assert reg.status_code == 201
# 2. Login
login = client.post("/auth/login", json={
"email": "e2e@test.com",
"password": "secure123"
})
assert login.status_code == 200
token = login.json()["access_token"]
# 3. Acceso protegido
headers = {"Authorization": f"Bearer {token}"}
resp = client.get("/me", headers=headers)
assert resp.status_code == 200
assert resp.json()["email"] == "e2e@test.com"
2. Parent → child → parent con hijos
def test_create_parent_then_child_then_fetch_parent_with_children(self, client):
parent = client.post("/categories", json={"name": "Electronics"})
parent_id = parent.json()["id"]
child = client.post("/categories", json={
"name": "Phones",
"parent_id": parent_id
})
assert child.status_code == 201
parent_with_children = client.get(f"/categories/{parent_id}?include_children=true")
assert len(parent_with_children.json()["children"]) == 1
3. Idempotencia
def test_double_create_with_same_idempotency_key_returns_same_resource(self, client):
key = "idem-123"
r1 = client.post("/orders", json={"items": [1, 2]}, headers={"Idempotency-Key": key})
r2 = client.post("/orders", json={"items": [1, 2]}, headers={"Idempotency-Key": key})
assert r1.status_code == 201
assert r2.status_code == 200 # o 201 si es idempotent create
assert r1.json()["id"] == r2.json()["id"]
4. Flujos de error
def test_create_invalid_then_fix_then_succeed(self, client):
# Intento inválido
r1 = client.post("/items", json={"name": "", "price": -1})
assert r1.status_code == 422
# Correcto
r2 = client.post("/items", json={"name": "Valid", "price": 10.0})
assert r2.status_code == 201
# Recuperar inexistente
r3 = client.get("/items/99999")
assert r3.status_code == 404
Cuándo Vale la Pena el Costo de E2E
Dónde invertir
- Rutas críticas de negocio: checkout, registro, pago, onboarding.
- Flujos que cruzan varios componentes: auth + CRUD + notificaciones.
- Protección de regresiones: bugs que ya ocurrieron y quieres que no vuelvan.
- Contratos con clientes externos: si otra API depende de tu flujo completo.
Dónde no invertir
- Cada variación de cada endpoint: eso son integration tests.
- Edge cases de lógica pura: eso son unit tests.
- Escenarios hipotéticos poco probables: el ROI no compensa.
Regla práctica
Tener 2-5 flujos E2E por proyecto suele ser suficiente. Más que eso y la suite se vuelve lenta y frágil. Los integration tests cubren el resto.
Checklist: ¿E2E o integration?
¿El comportamiento que quieres verificar requiere múltiples requests encadenados?
→ Sí: E2E
→ No: Integration
¿El valor está en que el flujo completo funcione (registro → login → recurso)?
→ Sí: E2E
→ No: Integration
¿Solo necesitas verificar que un endpoint responde correctamente?
→ Integration
¿Quieres protección contra regresiones en un flujo crítico de negocio?
→ E2E
Organizar Tests E2E
Estructura de directorios
tests/
├── unit/
├── integration/
└── e2e/
├── test_item_lifecycle.py
├── test_user_auth_flow.py
└── test_order_checkout_flow.py
Convenciones de nombres
- Archivo: un archivo por flujo o historia de usuario.
- Tests:
test_*descriptivos que explican el flujo.
Ejemplos:
def test_full_item_crud_lifecycle(self): ...
def test_user_registration_flow(self): ...
def test_order_checkout_flow(self): ...
def test_create_parent_then_child_then_fetch_with_children(self): ...
Ejecutar E2E por separado (marcadores pytest)
Los E2E son más lentos. Puedes correrlos solo cuando haga falta:
# tests/e2e/test_item_lifecycle.py
import pytest
@pytest.mark.e2e
def test_full_crud_lifecycle(client):
...
# Solo unit e integration (rápido)
pytest -m "not e2e"
# Solo E2E
pytest -m e2e
# Todo
pytest
En pytest.ini o pyproject.toml:
[pytest]
markers =
e2e: End-to-end tests (slower, full flows)
Claude Code para Generar E2E Tests
Prompt base
Genera E2E tests que validen el flujo completo de [descripción del flujo].
Cada test debe simular un usuario real usando la API de principio a fin.
Usa FastAPI TestClient. No uses Selenium ni Playwright — solo HTTP.
Incluye setup para limpiar el estado entre tests (ej. clear DB o fixture).
Ejemplo concreto
Genera E2E tests para el flujo CRUD de items en mi API FastAPI.
Flujo: POST crear item → GET por id → PUT actualizar → DELETE → GET verificar 404.
Usa TestClient. Un test por flujo completo.
Incluye setup_function que limpie items_db antes de cada test.
Diferencias con integration
| Nivel | Prompt típico |
|---|---|
| Integration | "Genera integration tests para POST /items. Un test por endpoint, verifica status y body." |
| E2E | "Genera un E2E test que valide el flujo completo: crear → leer → actualizar → eliminar → verificar eliminado." |
La Trampa del E2E: Flakiness
Qué es el flakiness
Tests que a veces pasan y a veces fallan sin cambiar el código. Es más frecuente en E2E porque intervienen más componentes.
Causas habituales
- Estado compartido: tests que modifican la misma DB o el mismo recurso.
- Orden de ejecución: un test que depende de datos creados por otro.
- Timing: timeouts, delays, condiciones de carrera.
- Datos no deterministas: IDs, timestamps, aleatoriedad.
Cómo evitarlo
- Estado limpio por test:
setup_functiono fixture que vacíe/resetee DB. - Tests independientes: cada test crea sus propios datos.
- Evitar orden de ejecución: ningún test depende de otro.
- Fixtures aislados: cada test tiene su cliente, DB, etc.
- Evitar sleep(): usar polling o mocks para tiempo.
Ejemplo de anti-patrón
# MAL: estado compartido entre tests
items_db = [] # global
def test_create_item():
client.post("/items", json={"name": "A"}) # id=1
# Si otro test ya creó items, el id puede no ser 1
def test_get_item():
r = client.get("/items/1") # Asume que existe id=1
assert r.status_code == 200 # Falla si test_create_item no corrió antes
Patrón correcto
def setup_function():
items_db.clear()
def test_full_lifecycle():
create = client.post("/items", json={"name": "A"})
item_id = create.json()["id"] # Usa el id del response
# ... resto del flujo con item_id
Fixtures para E2E con Base de Datos Real
Cuando tu API usa PostgreSQL, SQLite u otra DB, los E2E necesitan un estado limpio sin afectar datos de desarrollo.
Opción 1: DB en memoria (SQLite)
# conftest.py
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.database import Base, get_db
from main import app
engine = create_engine(
"sqlite:///:memory:",
connect_args={"check_same_thread": False}
)
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
def override_get_db():
db = TestingSessionLocal()
try:
yield db
finally:
db.close()
@pytest.fixture(scope="function")
def client():
Base.metadata.create_all(bind=engine)
app.dependency_overrides[get_db] = override_get_db
with TestClient(app) as c:
yield c
Base.metadata.drop_all(bind=engine)
app.dependency_overrides.clear()
Cada test usa una DB vacía en memoria. Rápido y aislado.
Opción 2: Transacciones que se revierten
from sqlalchemy.orm import Session
@pytest.fixture
def db_session():
"""Crea una transacción que se hace rollback después del test."""
connection = engine.connect()
transaction = connection.begin()
session = Session(bind=connection)
yield session
session.close()
transaction.rollback()
connection.close()
Cada test corre en una transacción que nunca se hace commit. Al final, rollback — la DB queda igual que al inicio.
Opción 3: Limpiar tablas en setup
from sqlalchemy import text
def setup_function():
with engine.connect() as conn:
conn.execute(text("DELETE FROM items"))
conn.execute(text("DELETE FROM categories"))
conn.commit()
Más simple pero menos aislado si hay foreign keys o triggers. Úsalo cuando no tengas transacciones disponibles.
Integración vs E2E: Comparación Lado a Lado
Misma funcionalidad (CRUD de items), dos enfoques:
Integration: endpoints aislados
# tests/integration/test_items_api.py
def test_post_items_returns_201(client):
r = client.post("/items", json={"name": "Item", "price": 10.0})
assert r.status_code == 201
assert "id" in r.json()
def test_get_item_returns_200(client):
create = client.post("/items", json={"name": "X", "price": 5.0})
item_id = create.json()["id"]
r = client.get(f"/items/{item_id}")
assert r.status_code == 200
def test_put_item_returns_200(client):
create = client.post("/items", json={"name": "Y", "price": 1.0})
item_id = create.json()["id"]
r = client.put(f"/items/{item_id}", json={"name": "Z", "price": 2.0})
assert r.status_code == 200
def test_delete_item_returns_200(client):
create = client.post("/items", json={"name": "W", "price": 0.0})
item_id = create.json()["id"]
r = client.delete(f"/items/{item_id}")
assert r.status_code == 200
4 tests, cada uno verifica un endpoint. Si PUT falla pero POST y GET funcionan, solo falla test_put_item. No comprueban que el flujo completo sea coherente.
E2E: flujo completo
# tests/e2e/test_item_lifecycle.py
def test_full_crud_lifecycle(client):
# Todo en un solo test, orden real de uso
create = client.post("/items", json={"name": "E2E", "price": 42.0})
assert create.status_code == 201
item_id = create.json()["id"]
get1 = client.get(f"/items/{item_id}")
assert get1.status_code == 200
assert get1.json()["name"] == "E2E"
update = client.put(f"/items/{item_id}", json={"name": "Updated", "price": 99.0})
assert update.status_code == 200
get2 = client.get(f"/items/{item_id}")
assert get2.json()["name"] == "Updated"
delete = client.delete(f"/items/{item_id}")
assert delete.status_code == 200
get3 = client.get(f"/items/{item_id}")
assert get3.status_code == 404
1 test que valida el flujo completo. Si DELETE no borra realmente y GET sigue devolviendo 200, este test falla. Los integration tests individuales podrían seguir en verde.
Ejercicios
Ejercicio 1: Diferenciar Integration y E2E (Fácil)
Indica si cada descripción corresponde a un test de Integration o E2E:
- Verifica que POST /users retorna 201 con el usuario en el body.
- Verifica que tras registrar usuario, hacer login y crear un post, el post aparece en GET /users/me/posts.
- Verifica que GET /products/999 retorna 404.
- Verifica el flujo: crear categoría → crear producto con esa categoría → obtener categoría con productos incluidos.
Ver solución
- Integration — Un solo endpoint, un solo request.
- E2E — Secuencia: registrar → login → crear post → GET.
- Integration — Un solo GET.
- E2E — Flujo de múltiples requests con dependencia entre ellos.
Ejercicio 2: E2E para flujo de auth (Medio)
Tienes endpoints: POST /register, POST /login, GET /me (protegido con Bearer). Escribe un E2E test que valide: registro → login → acceso a /me con el token.
Ver solución
def test_register_login_and_access_me(client):
# 1. Register
reg = client.post("/register", json={
"email": "e2e@test.com",
"password": "secret123"
})
assert reg.status_code == 201
# 2. Login
login = client.post("/login", json={
"email": "e2e@test.com",
"password": "secret123"
})
assert login.status_code == 200
token = login.json()["access_token"]
# 3. Access protected resource
me = client.get("/me", headers={"Authorization": f"Bearer {token}"})
assert me.status_code == 200
assert me.json()["email"] == "e2e@test.com"
Ejercicio 3: Setup para evitar flakiness (Medio)
Un E2E test falla aleatoriamente. Sospechas estado compartido. El test crea items y a veces falla en assert create.json()["id"] == 1. ¿Qué cambios harías?
Ver solución
Problema: Asume que el primer item creado tiene id == 1. Si otros tests o ejecuciones previas dejaron datos, el id puede ser mayor.
Cambios:
- Limpieza antes del test:
def setup_function():
items_db.clear() # o truncate de la tabla en DB real
- No asumir id fijo:
create = client.post("/items", json={"name": "X", "price": 1.0})
item_id = create.json()["id"] # Usa el id devuelto
# Usa item_id en el resto del test
- Fixtures aislados: Asegurar que cada test tenga su propia DB o transacción que se revierte.
Ejercicio 4: Escribir un E2E completo (Medio)
Implementa un E2E test para una API de tasks con POST /tasks, GET /tasks/{id}, PATCH /tasks/{id}, DELETE /tasks/{id}. El flujo: crear → leer → actualizar (marcar completed) → eliminar → verificar 404.
Ver solución
def test_full_task_lifecycle(client):
# Create
create = client.post("/tasks", json={"title": "E2E task"})
assert create.status_code == 201
task_id = create.json()["id"]
# Read
get1 = client.get(f"/tasks/{task_id}")
assert get1.status_code == 200
assert get1.json()["title"] == "E2E task"
assert get1.json()["completed"] is False
# Update
patch = client.patch(f"/tasks/{task_id}", json={"completed": True})
assert patch.status_code == 200
assert patch.json()["completed"] is True
# Delete
delete = client.delete(f"/tasks/{task_id}")
assert delete.status_code in (200, 204)
# Verify gone
get2 = client.get(f"/tasks/{task_id}")
assert get2.status_code == 404
Ejercicio 5: Prompt para Claude Code (Fácil)
Quieres que Claude Code genere E2E tests para el flujo de checkout: agregar items al carrito → aplicar cupón → confirmar orden → verificar que la orden existe. Escribe el prompt.
Ver solución
Genera E2E tests que validen el flujo completo de checkout de mi API.
Flujo: POST /cart/items (agregar items) → POST /cart/coupon (aplicar cupón) → POST /orders (confirmar) → GET /orders/{id} (verificar orden creada).
Usa FastAPI TestClient. Un test por flujo completo.
Cada test debe crear su propio carrito (o usar setup que limpie el estado).
No uses Selenium ni Playwright — solo HTTP contra la API.
Ejercicio 6: ¿E2E o Integration? (Medio)
Para cada escenario, di si elegirías E2E o Integration y por qué:
- a) Verificar que un cambio en la validación de email en registro rompe el flujo registro → login.
- b) Verificar que PUT /users/{id} retorna 400 cuando el email ya existe.
- c) Verificar que tras crear 10 órdenes, GET /orders devuelve las 10.
Ver solución
- a) E2E — El flujo completo (registro → login) es lo que importa. Un integration de POST /register podría no cubrir bien la interdependencia.
- b) Integration — Un solo endpoint, un solo request con caso de error. No necesitas encadenar requests.
- c) Integration o E2E — Depende. Si solo verificas que GET devuelve lo creado, es integration (crear + GET). Si verificas un flujo más amplio (crear → pagar → listar órdenes completadas), sería E2E. Para "crear 10 y listar" suele bastar integration.
Conexión con Proyecto
El proyecto del módulo requiere una test pyramid completa para una API REST de items. Según la cápsula 01, debes incluir:
- Unit tests para lógica de negocio.
- Integration tests para cada endpoint.
- Al menos 2 E2E tests que validen flujos completos.
Ejemplos de E2E que encajan:
- Flujo CRUD de items: crear → leer → actualizar → eliminar → verificar 404.
- Flujo con categorías (si aplica): crear categoría → crear item con categoría → obtener item con categoría.
Claude Code puede generar ambos con prompts como los de esta cápsula. Asegúrate de tener setup (p. ej. setup_function o fixtures) que limpien el estado entre tests para evitar flakiness.
Troubleshooting
1. El E2E falla solo a veces (flaky)
Causa: Estado compartido, orden de ejecución o timing.
Solución: Añade setup_function o fixture que vacíe la DB/storage antes de cada test. Asegúrate de que cada test cree sus propios datos y no dependa de otros tests. Evita sleep(); si necesitas esperar, usa polling o mocks.
2. El test asume ids fijos (1, 2, 3…)
Causa: Código que usa client.get("/items/1") asumiendo que siempre existirá.
Solución: Usa siempre el id devuelto por el request anterior: item_id = create.json()["id"] y luego client.get(f"/items/{item_id}").
3. Los E2E tardan demasiado
Causa: Demasiados E2E o setup pesado (DB real, servicios externos).
Solución: Reduce a 2–5 flujos E2E críticos. Usa DB en memoria o SQLite para tests. Mueve variaciones de endpoints a integration tests.
4. Claude Code genera tests de Selenium/Playwright
Causa: El prompt no aclara que E2E es solo API, no UI.
Solución: Sé explícito: "E2E para API: flujos HTTP completos con TestClient. No uses Selenium, Playwright ni browser. Solo requests HTTP contra el backend."
5. El test pasa pero el flujo falla en producción
Causa: TestClient no pasa por el mismo stack que producción (proxy, load balancer, middleware, CORS).
Solución: Los E2E con TestClient cubren la app tal como la tienes montada en tests. Para detectar problemas de infraestructura, necesitarías tests contra un entorno más cercano a producción (staging). Para la mayoría de proyectos, E2E con TestClient es suficiente para validar la lógica del flujo.
Resumen
- ✅ E2E para APIs = flujos completos vía HTTP, no testing de browser; no Selenium ni Playwright
- ✅ Integration verifica un endpoint aislado; E2E verifica una secuencia de requests como cliente real
- ✅ E2E con TestClient: crear → leer → actualizar → eliminar → verificar eliminación
- ✅ Patrones habituales: registro → login → recurso protegido; parent → child → parent con hijos; idempotencia; flujos de error
- ✅ Usa E2E en flujos críticos (2–5 por proyecto); el resto con integration y unit
- ✅ Organiza en
tests/e2e/con nombres que describan el flujo - ✅ Claude Code genera E2E con prompts que especifican "flujo completo" y "solo HTTP"
- ✅ Flakiness: evitar estado compartido, limpiar en setup, tests independientes, no asumir ids fijos
Próxima cápsula: Proyecto — Test pyramid completa (Cápsula 06).
Recursos Adicionales
- FastAPI: Testing — Documentación oficial de TestClient
- Martin Fowler: E2E Testing — Broad stack tests vs narrow
- Google: Just Say No to More E2E Tests — Cuándo no abusar de E2E
- pytest: Fixing flaky tests — Estrategias para tests inestables
- httpx: Async client — Cliente HTTP para tests más avanzados
- Test Pyramid vs Ice Cream Cone — Por qué no invertir la pyramid con demasiados E2E
Módulo 3, Cápsula 05 — Testing with Claude Code Guide