Una prueba de registro no termina cuando el endpoint responde 202. Todavía hay que saber si se generó el email correcto, si llegó el mensaje esperado y si el código que se consumió pertenecía a la ejecución actual. Cuando esa información queda escondida en logs dispersos, una prueba que falla se vuelve dificil de investigar.
En proyectos Python y FastAPI me resulta útil tratar cada verificación como una pequeña operación con recibo. El recibo no guarda el contenido completo del correo: guarda las señales necesarias para explicar qué ocurrió. Este patrón hace que la Automatización sea más repetible y que el equipo pueda depurar sin abrir bandejas a ciegas.
También ayuda a separar el código de negocio del proveedor de correo. En una búsqueda o incidencia pueden aparecer términos ruidosos como tp mail so y temp mail.so, además de errores escritos como tem email o tepm mail com. El servicio no debería tomar esas cadenas como una integración válida; deben quedarse como entrada de prueba o texto de búsqueda.
Por qué una prueba necesita un recibo
Un True en una aserción no cuenta toda la historia. Para una prueba de email suelen importar al menos cuatro hechos:
- qué ejecución creó el buzón o alias;
- cuándo se solicitó el mensaje;
- qué identificador o asunto se encontró;
- cuándo se consumió el código y con qué resultado.
Sin esas señales, un test puede pasar leyendo un mensaje viejo. También puede fallar porque el proveedor tardó, aunque la aplicación sí haya hecho lo correcto. El problema se agrava cuando varios jobs comparten un inbox: los mensajes se mezcla y el fallo parece aleatorio.
Por eso conviene devolver un recibo pequeño desde la API. Un recibo permite guardar run_id, message_id, status y marcas de tiempo, sin copiar el cuerpo del email a los logs de CI.
El contrato mínimo del recibo
Antes de conectar un proveedor real, define un contrato que se pueda probar en memoria. Una respuesta útil podría tener esta forma:
{
"run_id": "checkout-1842",
"message_id": "msg-91",
"status": "verified",
"requested_at": "2026-10-15T06:00:00Z",
"observed_at": "2026-10-15T06:00:04Z"
}
Los estados deben ser pocos y explícitos: created, waiting, verified, expired y failed suelen bastar. Si el cliente recibe waiting, puede continuar el polling; si recibe expired, debe crear una nueva ejecución, no reintentar indefinidamente.
El run_id es la frontera principal. El adaptador de correo debe buscar mensajes posteriores a requested_at y asociados al alias de esa ejecución. Para evitar filtraciones, el recibo puede incluir un hash o un identificador externo, pero no el token completo.
Este contrato complementa una estrategia para separar emails de signup por escenario: cada escenario tiene una identidad que se puede comprobar de forma independiente.
Implementación en FastAPI
El endpoint puede delegar la entrega en un proveedor con una interfaz pequeña. Así, el test unitario no depende de una bandeja remota.
from datetime import datetime, timezone
from typing import Protocol
from fastapi import FastAPI
from pydantic import BaseModel
class DeliveryReceipt(BaseModel):
run_id: str
message_id: str | None = None
status: str
requested_at: datetime
observed_at: datetime | None = None
class MailProvider(Protocol):
def request(self, run_id: str) -> DeliveryReceipt: ...
class FakeMailProvider:
def request(self, run_id: str) -> DeliveryReceipt:
now = datetime.now(timezone.utc)
return DeliveryReceipt(
run_id=run_id,
status="waiting",
requested_at=now,
)
app = FastAPI()
provider = FakeMailProvider()
@app.post("/email-checks", response_model=DeliveryReceipt)
def create_email_check(run_id: str) -> DeliveryReceipt:
return provider.request(run_id)
En un proveedor real, request puede crear el alias y registrar la hora de inicio. Otro método, como observe, buscaría el mensaje correcto y devolvería un nuevo recibo con message_id y observed_at. La validación del run_id debe ocurrir antes de crear recursos; un identificador vacío hace el diagnostico mas confuso.
Cómo filtrar y depurar en CI
El polling debería tener una fecha límite y una razón de salida. En lugar de repetir una petición hasta que algo responda, registra eventos como message_not_seen, message_wrong_run o expired. Son nombres aburridos, pero explican mucho mejor un fallo.
Para cada job de CI, guarda solo un artefacto con:
-
run_idy nombre del escenario; - estado final y número de intentos;
-
requested_at,observed_aty tiempo total; - identificadores redactados del alias y del mensaje.
En pruebas de integración, usa un proveedor fake para comprobar transiciones y deja una prueba pequeña para la integración externa. Así puedes distinguir un bug de FastAPI de una demora del servicio. Si el pipeline reintenta, conserva el mismo run_id solo cuando el contrato sea idempotente; de lo contrario, crea una ejecución nueva.
Si el equipo necesita más contexto, los contratos de email para pruebas repetibles ayudan a convertir los criterios de entrega en comprobaciones concretas. Un generador de direcciones de correo desechables puede servir para escenarios aislados, pero la prueba debe seguir controlando TTL, datos permitidos y limpieza; el proveedor no sustituye el contrato.
Preguntas frecuentes
¿Debo guardar el contenido del email?
Normalmente no. Un identificador de mensaje, el estado y las marcas de tiempo ofrecen contexto suficiente. Si necesitas inspeccionar el cuerpo, redacta tokens y direcciones antes de enviarlo a los logs.
¿Un recibo reemplaza la aserción del test?
No. El recibo aporta evidencia; la aserción sigue verificando el comportamiento esperado. La ventaja es que, cuando la aserción falla, sabes si falló la creación, la entrega, el filtrado o el consumo.
¿Cuánto tiempo debe esperar el polling?
El límite depende del proveedor y del entorno, pero debe estar explícito y ser configurable. Un timeout claro es mas útil que un job que queda esperando hasta que CI lo mata.
Un recibo de email no necesita una plataforma grande. Con un run_id, estados pequeños, marcas de tiempo y un adaptador aislado, FastAPI puede ofrecer pruebas más previsibles y una evidencia que ayuda de verdad cuando algo falla.
Top comments (0)