Módulo 7 · Pruebas

Lección 35 — Pruebas E2E y de contrato

El flujo completo de compra bajo prueba y contratos que no se rompen.

Publicada
En esta lección
  1. Objetivos
  2. 1. Qué es E2E aquí (y qué no)
  3. 2. El catálogo E2E: pocas y valiosas
  4. 3. Estabilidad: el flakiness es un bug
  5. 4. Pruebas de contrato: el front falla antes
  6. 5. La sala de máquinas del E2E
  7. Autoevaluación

Stack: Django/DRF · Proyecto: TicketFlow Estado: Publicada — el flujo de compra completo bajo prueba Prerrequisito: Lección 34 — Pruebas de integración


Objetivos

  1. Cubrir el flujo completo de compra (buscar → reservar → pagar → confirmar → tickets) con pocas pruebas E2E rápidas y estables.
  2. Fijar los contratos entre equipos con pruebas de consumidor (pact-style) que fallan antes que el front.
  3. Decidir qué NO probar en E2E: la pirámide se sostiene porque la cúpula es pequeña.

1. Qué es E2E aquí (y qué no)

E2E = el sistema desplegado como en producción (app + Postgres + Redis reales), ejercitado por su interfaz real (HTTP), con las dependencias externas dobladas SOLO en la frontera del negocio (pasarela sandbox, SMTP fake). No es "abrir un navegador con Selenium para comprobar que el botón es azul": TicketFlow es una API; su E2E es un cliente HTTP que ejecuta la HISTORIA del usuario. La diferencia con la integración (34): allí probabas UNA costura (el lock); aquí la HISTORIA completa — el valor entero: "compro una entrada y llega confirmada".

python
# tests/e2e/test_compra_completa.py
def test_compra_feliz(client_api, pasarela_sandbox, mailbox):
    r1 = client_api.post("/api/v1/events?city=Madrid&date=2027-03-01")
    assert r1.status_code == 200
    evento = r1.json()["items"][0]

    r2 = client_api.post("/api/v1/reservations", {evento: evento["id"], "seats": ["A1", "A2"]})
    assert r2.status_code == 201
    ref = r2.json()["public_ref"]

    r3 = client_api.post(f"/api/v1/reservations/{ref}/pay", {"method": "card"})
    assert r3.status_code == 200

    drenar_cola()                                     # eager mode del worker (29)
    r4 = client_api.get(f"/api/v1/reservations/{ref}")
    assert r4.json()["status"] == "CONFIRMED"          # la historia terminó bien
    assert len(mailbox) == 1

El drenar_cola() es la pieza honesta de asincronía en tests: el worker corre en eager dentro del proceso; el test espera a que la historia CONVERGE (la garantía eventual de la 32), no a un sleep.

2. El catálogo E2E: pocas y valiosas

El presupuesto: <20 tests E2E, las historias de negocio críticas: compra feliz, compra con declinación → compensación (32), compra con asiento disputado (un asiento, un ganador), expiración de reserva durante el pago, export RGPD (23), webhook entrante de la pasarela (17), recovery de saga (UNKNOWN). Cada una es la póliza de un flujo que vende el producto. Lo que NO sube a E2E: casos límite de validación (400 con cada campo mal — son unitarias del serializer), permisos por rol (unitarias/integración de permissions), rendimiento (36). El error del E2E-obeso: 400 tests E2E "porque es lo real" — suite de 90 min, frágil (cada test depende del estado global), nadie la mantiene: la cúpula se hunde.

python
# la marca que separa y el presupuesto en CI (41)
@pytest.mark.e2e
def test_compra_feliz(...): ...
# pytest -m "not e2e" en el pre-push; pytest -m e2e en el pipeline

3. Estabilidad: el flakiness es un bug

Un test E2E que falla "a veces" destruye la suite entera: el equipo aprende a re-relear y en tres meses nadie mira el rojo. Las tres causas y sus curas: (1) asincronía: sleep → drenar_cola/espera activa con timeout (wait_for(lambda: saga.estado == "CONFIRMED") con deadline de 5 s y fallo con dump de estado); (2) orden de datos: cada test crea SU evento/usuario (baker), nunca un fixture "global sembrado" en la BD; (3) recursos compartidos: puertos, ficheros temporales, Redis DB 15 (34). La regla del equipo: un test que falla 2 veces sin causa clara se QUARANTINA (@pytest.mark.flaky en CI o marcador propio) con issue abierto — no se borra ni se ignora: se cura o se amaestra.

python
def wait_for(cond, timeout=5.0, msg="condición no llegó"):
    fin = time.monotonic() + timeout
    while time.monotonic() < fin:
        if cond():
            return
        time.sleep(0.05)
    raise AssertionError(f"{msg}: dump={dump_estado()}")

Espera activa con deadline y dump: el fallo del test se lee solo (qué saga, qué estado, qué cola pendiente), no se investiga con prints.

4. Pruebas de contrato: el front falla antes

El contrato entre el backend de TicketFlow y el front (o la app móvil) se rompe en silencio: renombras seat_refs → seats (28) y el rojo aparece en producción, en el teléfono del comprador. La prueba de contrato fija el acuerdo ANTES: el CONSUMIDOR describe lo que usa y el PROVIDER la verifica. Formato pact-style (en su versión mínima, tests nativos):

python
# tests/contract/test_front_puede_comprar.py — el consumidor (front) define:
CONTRATO_FRONT = {
    "POST /api/v1/reservations": {
        "request": {"event": "uuid", "seats": ["str"]},
        "response": {"status": 201, "body": {"public_ref": "str", "seats": ["str"],
                                              "total": "int", "expires_at": "iso8601"}},
    },
    "GET /api/v1/reservations/{ref}": {"response": {"status": 200, "body": {"status": "str"}}},
}

@pytest.mark.contract
@pytest.mark.parametrize("interaccion", CONTRATO_FRONT.items(), ids=lambda kv: kv[0])
def test_el_front_puede(interaccion, client_api, evento_publicado):
    ruta, esperado = interaccion
    ...ejecuta la petición según el contrato y valida status + shape del body...

Regla de oro: el contrato prueba lo que el CONSUMIDOR USA, no todo el body — un campo nuevo no rompe (backwards compatible, 14); un campo usado que cambia, sí. En equipo de 1 (el curso), el contrato te protege de TI mismo en 6 meses; en equipo, es el acuerdo ejecutable que sustituye la reunion de "¿el front ya adaptó el campo?".

5. La sala de máquinas del E2E

La infraestructura que hace posible la cúpula: (1) el entorno de test aislado (compose con app+PG+Redis, la 40 lo formaliza con depends_on: condition: service_healthy); (2) el drenado determinista de colas (eager mode o worker real con wait_for); (3) el usuario y el token de test (18: fixture que crea usuario y saca access token, sin login "manual"); (4) el sandbox de pasarela (FakeGateway montado como app o la sandbox real si tiene modo test — la 14 le da idempotencia); (5) el reset limpio entre tests (truncado de tablas + flush de Redis, no "reiniciar el mundo" — 30 s vs 3 min por test).

La métrica de salud del E2E: flaky_rate = fallos-no-causa / ejecuciones < 1%. Y la regla de la sala: si mañana el negocio pregunta "¿está roto el checkout?", la respuesta es pytest -m e2e -k compra en 8 minutos — no "probemos en staging a ver".


Autoevaluación

  1. ¿Qué diferencia a la E2E de la integración y por qué el E2E dobla solo las dependencias del NEGOCIO (pasarela) y no las de infra (Postgres)?
  2. Presupuesto de <20 E2E: ¿qué historias entran en el catálogo de TicketFlow y qué pruebas quedan FUERA deliberadamente?
  3. Las tres causas del flakiness y su cura: ¿por qué un test flaky se contagia a toda la suite?
  4. En la prueba de contrato, ¿qué define el consumidor y qué hace el provider? ¿Por qué un campo NUEVO no rompe el contrato?
  5. wait_for con deadline y dump: ¿qué sustituye y qué aporta al diagnóstico del fallo?

Continúa con los ejercicios. Las solutions.md solo tras intentarlo.