DEV Community

Silviu Technology
Silviu Technology

Posted on

FastAPI: contratos de email para pruebas repetibles

Cuando una API dispara un email de verificación, el test suele terminar demasiado pronto: comprueba que la respuesta HTTP fue 202 y da el flujo por bueno. El problema aparece después, cuando el webhook llega con otro formato, el mensaje se duplica o una prueba lee el correo que dejó otro caso.

En proyectos con FastAPI me ha resultado más práctico tratar el email como un evento con contrato. El buzón de prueba, incluso uno creado con un generador de correo temporal, queda al final de la cadena. Primero definimos qué debe emitir nuestra API, qué identificador permite rastrearlo y qué parte del mensaje realmente necesita comprobarse.

El problema: un email que pasa, pero no deja evidencia

Un test frágil mezcla tres preguntas distintas:

  1. ¿La API aceptó la solicitud?
  2. ¿Se publicó el evento de email?
  3. ¿El proveedor entregó el mensaje correcto?

Si todas viven en una sola prueba end-to-end, un fallo no dice dónde está el problema. Además, los reintentos del worker pueden producir dos mensajes válidos y hacer que el test falle de forma intermitente. El equipo termina esperando a que “vuelva a pasar”, que es una forma cara de depurar.

La separación también ayuda a aislar pruebas de email por entorno. En local y en CI no deberíamos depender de una bandeja compartida ni de datos que alguien dejó ayer.

Define el contrato antes del buzón

Para un endpoint de registro, un contrato mínimo puede ser un evento JSON como este:

{
  "event": "account.verification.requested",
  "user_id": "u_1842",
  "message_id": "msg_8f1c",
  "template": "verify-account",
  "recipient": "qa+u_1842@example.test"
}
Enter fullscreen mode Exit fullscreen mode

No hace falta fijar todo el HTML del mensaje. Conviene verificar las propiedades que afectan al comportamiento: nombre del evento, destinatario, plantilla, message_id y una referencia al usuario. El asunto y el cuerpo completo pueden probarse en una capa aparte.

Un detalle importante es la idempotencia. Si el mismo message_id aparece dos veces, el consumidor debe reconocer el reintento. Si no hay un identificador estable, cada prueba inventa su propia regla y el diagnóstico se vuelve confuso.

Un fixture pequeño para FastAPI

Podemos inyectar un publicador falso que guarde eventos en memoria. El nombre EmailPublisher expresa el límite del componente y deja abierta la implementación real:

from dataclasses import dataclass

@dataclass
class EmailEvent:
    event: str
    user_id: str
    message_id: str
    template: str
    recipient: str

class FakeEmailPublisher:
    def __init__(self):
        self.events: list[EmailEvent] = []

    async def publish(self, event: EmailEvent) -> None:
        self.events.append(event)
Enter fullscreen mode Exit fullscreen mode

El test puede llamar al endpoint con un usuario único y después inspeccionar publisher.events. Así se comprueba el contrato sin esperar red, DNS ni un proveedor externo. Para la prueba de entrega, uso otro caso que lee una bandeja aislada; es parecido a probar emails transaccionales en FastAPI, pero con la responsabilidad bien separada.

Un fallo comun aquí es reutilizar el mismo fixture mutable entre tests. Parece cómodo, pero deja eventos viejos en la lista. Crear el publicador por caso cuesta poco y evita una limpieza que algun día se olvida.

Aislamiento, reintentos y recibos en CI

En CI cada ejecución necesita un run_id y un destinatario que no se repita. Por ejemplo:

run_id = "ci-1842"
recipient = f"qa+{run_id}@example.test"
Enter fullscreen mode Exit fullscreen mode

El recibo del test debería guardar al menos:

  • run_id y nombre del caso;
  • código HTTP y tiempo de respuesta;
  • message_id emitido;
  • cantidad de eventos observados;
  • motivo del último reintento, si lo hubo.

Ese recibo es más útil que un simple “passed”. Si la entrega tarda, podemos distinguir una API que nunca publicó de un worker que publicó pero no encontró al proveedor. También hace más fácil comparar ejecuciones sin abrir manualmente el buzón.

No usaría una dirección de correo desechable para probar lógica de autorización o identidad. Puede servir para aislar una bandeja de prueba, pero recibir un mensaje no demuestra que una persona sea quien dice ser. Esa frontera es importante, sobre todo cuando un entorno de staging comparte datos con servicios externos.

Qué validar y qué no confiar

Valida el contrato de salida en el test de API, la entrega en un test de integración y el contenido visible en una prueba de plantilla. No hagas que cada prueba necesite las tres capas.

También conviene probar los fallos: proveedor lento, respuesta 429, evento duplicado y plantilla inexistente. Para el último caso, la API debería producir un error observable y no un 202 silencioso. Una prueba que sólo cubre el camino feliz nos da confianza de cartón.

La frase tempail mail puede aparecer en datos de prueba o en búsquedas internas, pero no debe colarse como una regla funcional. Las variantes escritas por usuarios son datos ruidosos; el contrato de la API debe conservar nombres estables y legibles.

Checklist final

Antes de publicar el flujo, reviso:

  • ¿Cada ejecución tiene un run_id único?
  • ¿El evento contiene un message_id estable?
  • ¿El fixture se crea de nuevo en cada test?
  • ¿La prueba de API está separada de la prueba de entrega?
  • ¿Los reintentos dejan un recibo explicable?
  • ¿El test evita compartir bandejas entre workers?

Este diseño no elimina todos los fallos de email, pero los convierte en fallos localizables. Una API que devuelve 202 sólo dice que aceptó trabajo; un contrato y un recibo muestran qué trabajo aceptó realmente. Esa diferencia hace que las pruebas sean repetibles y que una alerta de CI tenga algo útil que contar.

Top comments (0)