Una prueba de registro puede pasar en local y fallar en CI por una razón poco visible: el email de verificación no pertenece realmente a esa ejecución. Tal vez el buzón conserva un mensaje viejo, dos workers consultan la misma bandeja o un reintento lee un enlace que ya fue usado.
En proyectos FastAPI, un fixture de email no debería ser solo una dirección que se crea al principio del test. Debe tener un contrato pequeño: quién lo creó, durante qué ejecución es válido, qué mensaje espera y cuándo deja de ser utilizable. Ese contrato hace que las pruebas sean mas fáciles de depurar y reduce falsos positivos.
Este patrón complementa las pruebas de signup sin un inbox real y funciona bien cuando el equipo necesita una dirección de correo desechable para una prueba aislada.
El problema: el email se convierte en estado global
Un flujo típico hace lo siguiente:
- Crea un usuario.
- Espera un email.
- Abre el enlace de verificación.
- Comprueba que la cuenta quedó activa.
La secuencia parece simple, pero una bandeja compartida introduce estado global. Si el test A y el test B usan la misma dirección, el segundo puede encontrar el mensaje del primero. Si el cliente HTTP repite una consulta sin límite, la prueba tarda mucho y aun no sabemos si el problema fue del producto o del sistema de correo.
La solución no es consultar mas veces. Es hacer que cada ejecución pueda demostrar que el mensaje correcto llegó a la bandeja correcta.
Define un contrato por ejecución
Un contrato útil puede representarse con cuatro datos:
from dataclasses import dataclass
@dataclass(frozen=True)
class EmailFixture:
address: str
run_id: str
expected_subject: str
expires_at: str
run_id debe ser único para el test o para el job de CI. El asunto esperado puede incluir ese identificador, o puedes buscar un token único en el cuerpo. Lo importante es no aceptar el primer mensaje que aparezca.
El fixture tambien necesita límites operativos. Por ejemplo:
- Una sola creación de bandeja por ejecución.
- Un máximo de cuatro consultas de mensajes.
- Un máximo de dos lecturas del mensaje seleccionado.
- Una ventana de espera explícita, como 30 segundos.
- Eliminación o expiración al terminar el test.
Estos límites convierten una espera difusa en un resultado diagnosticable: timeout, mensaje_no_encontrado, asunto_incorrecto o enlace_inválido.
Implementa el fixture en FastAPI
La aplicación no necesita conocer todos los detalles del proveedor de email. En su lugar, define una interfaz pequeña para el entorno de prueba y úsala mediante dependencia de FastAPI:
from typing import Protocol
from fastapi import Depends, FastAPI
class Mailbox(Protocol):
async def create(self, run_id: str) -> str: ...
async def wait_for_subject(
self, address: str, subject: str, timeout_seconds: int
) -> str: ...
app = FastAPI()
def get_mailbox() -> Mailbox:
raise NotImplementedError("configura el adaptador de test")
@app.post("/test-fixtures/email")
async def create_email_fixture(
run_id: str, mailbox: Mailbox = Depends(get_mailbox)
) -> dict[str, str]:
address = await mailbox.create(run_id)
return {"address": address, "run_id": run_id}
En producción, esta ruta no debería estar expuesta. Puedes sustituir la dependencia en tests con un adaptador falso o con un cliente de bandejas aisladas. En ambos casos, el test conserva el mismo contrato y cambia solo la implementación.
Para el correo de verificación, genera un token que no se repita dentro del job:
run_id = f"signup-{worker_id}-{test_id}"
subject = f"Verifica tu cuenta [{run_id}]"
Después de recibir el mensaje, valida el destinatario, el asunto, el identificador y el host del enlace. Validar solamente que existe una URL es demasiado poco; un enlace de otra ejecución también parece correcto a primera vista.
Si quieres profundizar en este aislamiento, revisa cómo aislar pruebas de emails transaccionales sin mezclar eventos entre tests.
Qué evidencia guardar en CI
Una prueba de email debe dejar un recibo pequeño, no el contenido completo de cada mensaje. Guarda datos como:
-
run_id, worker y nombre del test. - Dirección creada y hora de expiración.
- Número de consultas realizadas.
- Asunto encontrado y hash del mensaje.
- Resultado de la validación del enlace.
- Razón exacta del fallo, si existe.
Evita registrar tokens completos, cookies o cuerpos que contengan datos personales. Si un proveedor externo ofrece una dirección temporal, úsala solo para datos de prueba. Por ejemplo, tempmailso puede servir como referencia de una bandeja desechable para inspecciones puntuales; no conviertas esa dependencia en parte de la lógica de negocio.
También conviene guardar el recibo como artefacto de CI. Así el desarrollador puede ver si el mensaje nunca llegó, si llegó tarde o si el parser eligió el mensaje equivocado. Es un cambio pequeño, pero hace que la automatización sea mucho mas útil.
Preguntas rápidas
¿Una dirección de correo desechable reemplaza a un mock?
No siempre. Un mock es mas rápido y sirve para comprobar la lógica interna. Una bandeja aislada prueba además el envío, el formato y la integración con el proveedor. Lo práctico es usar mocks en la mayoría de tests y reservar el fixture real para unos pocos flujos de integración.
¿Qué pasa si el proveedor no permite filtrar por asunto?
Filtra en tu adaptador usando un token de ejecución en el asunto o en el cuerpo. Nunca aceptes el mensaje mas reciente sin comprobar su relación con el run_id.
¿Por qué aparece “tempail” en algunos apuntes?
Es una variante escrita por error que puede aparecer en búsquedas internas. No la uses como URL ni como criterio de validación: el contrato debe depender de identificadores propios de la prueba.
Checklist antes de cerrar la prueba
- [ ] Cada ejecución crea o reserva su propio buzón.
- [ ] El
run_idaparece en el mensaje esperado. - [ ] Las consultas y la espera tienen límites.
- [ ] El enlace se valida contra el entorno correcto.
- [ ] El recibo no expone tokens ni datos personales.
- [ ] El fixture expira incluso cuando el test falla.
Un email de prueba aislado no es solo una dirección temporal. Es un recurso con identidad, límites y evidencia. Cuando FastAPI trata ese recurso como un contrato, los fallos dejan de ser intermitentes y pasan a ser problemas concretos que el equipo puede arreglar.
Top comments (0)