<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Olivia Cheng</title>
    <description>The latest articles on DEV Community by Olivia Cheng (@oliviachen7).</description>
    <link>https://dev.to/oliviachen7</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4013521%2Fa581805e-741a-4c70-9e67-dc5989491b5e.jpg</url>
      <title>DEV Community: Olivia Cheng</title>
      <link>https://dev.to/oliviachen7</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/oliviachen7"/>
    <language>en</language>
    <item>
      <title>FastAPI: contratos para emails de prueba aislados</title>
      <dc:creator>Olivia Cheng</dc:creator>
      <pubDate>Sun, 04 Oct 2026 14:23:43 +0000</pubDate>
      <link>https://dev.to/oliviachen7/fastapi-contratos-para-emails-de-prueba-aislados-2jb8</link>
      <guid>https://dev.to/oliviachen7/fastapi-contratos-para-emails-de-prueba-aislados-2jb8</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Este patrón complementa las &lt;a href="https://dev.to/silviutech/fastapi-pruebas-de-signup-sin-inbox-real-4jg9"&gt;pruebas de signup sin un inbox real&lt;/a&gt; y funciona bien cuando el equipo necesita una dirección de correo desechable para una prueba aislada.&lt;/p&gt;

&lt;h2&gt;
  
  
  El problema: el email se convierte en estado global
&lt;/h2&gt;

&lt;p&gt;Un flujo típico hace lo siguiente:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Crea un usuario.&lt;/li&gt;
&lt;li&gt;Espera un email.&lt;/li&gt;
&lt;li&gt;Abre el enlace de verificación.&lt;/li&gt;
&lt;li&gt;Comprueba que la cuenta quedó activa.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define un contrato por ejecución
&lt;/h2&gt;

&lt;p&gt;Un contrato útil puede representarse con cuatro datos:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;


&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;EmailFixture&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;expected_subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;expires_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;run_id&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;El fixture tambien necesita límites operativos. Por ejemplo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Una sola creación de bandeja por ejecución.&lt;/li&gt;
&lt;li&gt;Un máximo de cuatro consultas de mensajes.&lt;/li&gt;
&lt;li&gt;Un máximo de dos lecturas del mensaje seleccionado.&lt;/li&gt;
&lt;li&gt;Una ventana de espera explícita, como 30 segundos.&lt;/li&gt;
&lt;li&gt;Eliminación o expiración al terminar el test.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Estos límites convierten una espera difusa en un resultado diagnosticable: &lt;code&gt;timeout&lt;/code&gt;, &lt;code&gt;mensaje_no_encontrado&lt;/code&gt;, &lt;code&gt;asunto_incorrecto&lt;/code&gt; o &lt;code&gt;enlace_inválido&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementa el fixture en FastAPI
&lt;/h2&gt;

&lt;p&gt;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:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Protocol&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Mailbox&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Protocol&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;wait_for_subject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout_seconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;


&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_mailbox&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Mailbox&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;NotImplementedError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;configura el adaptador de test&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/test-fixtures/email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_email_fixture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mailbox&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Mailbox&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get_mailbox&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;address&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;mailbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;address&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Para el correo de verificación, genera un token que no se repita dentro del job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;run_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;signup-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;worker_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;test_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;subject&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Verifica tu cuenta [&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Si quieres profundizar en este aislamiento, revisa cómo &lt;a href="https://dev.to/silviutech/como-probar-emails-transaccionales-en-fastapi-sin-mezclar-bandejas-ni-eventos-5f0m"&gt;aislar pruebas de emails transaccionales&lt;/a&gt; sin mezclar eventos entre tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qué evidencia guardar en CI
&lt;/h2&gt;

&lt;p&gt;Una prueba de email debe dejar un recibo pequeño, no el contenido completo de cada mensaje. Guarda datos como:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;run_id&lt;/code&gt;, worker y nombre del test.&lt;/li&gt;
&lt;li&gt;Dirección creada y hora de expiración.&lt;/li&gt;
&lt;li&gt;Número de consultas realizadas.&lt;/li&gt;
&lt;li&gt;Asunto encontrado y hash del mensaje.&lt;/li&gt;
&lt;li&gt;Resultado de la validación del enlace.&lt;/li&gt;
&lt;li&gt;Razón exacta del fallo, si existe.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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, &lt;code&gt;tempmailso&lt;/code&gt; puede servir como referencia de una bandeja desechable para inspecciones puntuales; no conviertas esa dependencia en parte de la lógica de negocio.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preguntas rápidas
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ¿Una dirección de correo desechable reemplaza a un mock?
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Qué pasa si el proveedor no permite filtrar por asunto?
&lt;/h3&gt;

&lt;p&gt;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 &lt;code&gt;run_id&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Por qué aparece “tempail” en algunos apuntes?
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist antes de cerrar la prueba
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Cada ejecución crea o reserva su propio buzón.&lt;/li&gt;
&lt;li&gt;[ ] El &lt;code&gt;run_id&lt;/code&gt; aparece en el mensaje esperado.&lt;/li&gt;
&lt;li&gt;[ ] Las consultas y la espera tienen límites.&lt;/li&gt;
&lt;li&gt;[ ] El enlace se valida contra el entorno correcto.&lt;/li&gt;
&lt;li&gt;[ ] El recibo no expone tokens ni datos personales.&lt;/li&gt;
&lt;li&gt;[ ] El fixture expira incluso cuando el test falla.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;

</description>
      <category>python</category>
      <category>fastapi</category>
      <category>testing</category>
      <category>automation</category>
    </item>
    <item>
      <title>FastAPI: aislar pruebas de email con un contrato</title>
      <dc:creator>Olivia Cheng</dc:creator>
      <pubDate>Sun, 04 Oct 2026 08:24:00 +0000</pubDate>
      <link>https://dev.to/oliviachen7/fastapi-aislar-pruebas-de-email-con-un-contrato-3mdg</link>
      <guid>https://dev.to/oliviachen7/fastapi-aislar-pruebas-de-email-con-un-contrato-3mdg</guid>
      <description>&lt;p&gt;Cuando una prueba de registro falla porque encontró un correo viejo, normalmente no es un problema de FastAPI. Es un problema de aislamiento. En varios equipos he visto que se usa un buzón compartido para todo el pipeline y luego cada ejecución tiene que adivinar cual mensaje le pertenece. Funciona durante un tiempo, hasta que dos jobs corren juntos o llega un reintento tarde.&lt;/p&gt;

&lt;p&gt;Mi enfoque para estos flujos es tratar el buzón de prueba como un recurso con contrato: nace con la ejecución, tiene un identificador propio, expira y deja una evidencia pequeña antes de limpiarse. La idea sirve para una prueba de verificación, un flujo de recuperación de contraseña o cualquier API que dependa de un email entrante.&lt;/p&gt;

&lt;h2&gt;
  
  
  El problema: un buzón compartido contamina las pruebas
&lt;/h2&gt;

&lt;p&gt;Un test típico hace esto:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Crea un usuario con un correo de prueba.&lt;/li&gt;
&lt;li&gt;Llama al endpoint de signup de FastAPI.&lt;/li&gt;
&lt;li&gt;Espera el mensaje de verificación.&lt;/li&gt;
&lt;li&gt;Extrae el enlace y confirma la cuenta.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Si todas las ejecuciones usan la misma dirección, el paso tres puede leer un mensaje de otra rama o de un intento anterior. Un &lt;code&gt;subject&lt;/code&gt; parecido no alcanza: dos tests pueden tener el mismo asunto y destinatario. El fallo aparece de forma intermitente, que es la forma más cara de depurar.&lt;/p&gt;

&lt;p&gt;También conviene separar la preparación de datos del consumo del mensaje. Una seed list para un onboarding reproducible ayuda a ordenar usuarios y estados, pero no reemplaza un inbox aislado. La &lt;a href="https://dev.to/hannahdev56/saas-seed-lists-para-onboarding-4nfl"&gt;preparación de seed lists para un onboarding reproducible&lt;/a&gt; puede vivir en una etapa anterior; el contrato de correo debe seguir siendo específico para cada ejecución.&lt;/p&gt;

&lt;h2&gt;
  
  
  El contrato mínimo de un buzón por ejecución
&lt;/h2&gt;

&lt;p&gt;Antes de escribir código, defino cinco reglas sencillas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Identidad:&lt;/strong&gt; la dirección incluye un &lt;code&gt;run_id&lt;/code&gt; o un token aleatorio.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Propiedad:&lt;/strong&gt; el test conoce qué mensajes puede consumir.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Caducidad:&lt;/strong&gt; el recurso no debe quedarse vivo después del job.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Espera acotada:&lt;/strong&gt; nunca se espera para siempre.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Evidencia:&lt;/strong&gt; asunto, destinatario normalizado y resultado quedan en el log; no el contenido completo si contiene datos sensibles.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Esto es más útil que elegir un proveedor por la etiqueta de “throwaway email”. Incluso cuando se usa un burner email address para pruebas manuales, el pipeline necesita una política de propiedad y limpieza. Las variantes escritas como temp mailid o tempail aparecen a veces en búsquedas y tickets, pero no deben terminar mezcladas con el nombre de una fixture ni con un selector del test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Una fixture pequeña en Python
&lt;/h2&gt;

&lt;p&gt;La interfaz de la fixture puede ser independiente del proveedor. Así el test solo conoce las operaciones que necesita:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;uuid4&lt;/span&gt;


&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TestInbox&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_inbox&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qa.example&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;TestInbox&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;run_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;uuid4&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nb"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;TestInbox&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;signup-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;@&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;En un sistema real, &lt;code&gt;create_inbox&lt;/code&gt; también reservaría la dirección en el proveedor elegido. Lo importante es que devuelva el identificador que usaremos al buscar mensajes. No conviene generar una dirección en el test y buscar después por texto parcial: ese atajo hace que el aislamiento dependa de la suerte.&lt;/p&gt;

&lt;p&gt;El test puede pasar &lt;code&gt;inbox.address&lt;/code&gt; al endpoint y conservar &lt;code&gt;inbox.run_id&lt;/code&gt; como correlación. Para consultar el mensaje, filtraría por destinatario exacto, una marca de ejecución en los headers o un token único dentro del enlace. Si el token aparece en la URL, no lo imprimas completo en CI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cómo esperar y limpiar sin hacer el test frágil
&lt;/h2&gt;

&lt;p&gt;Leer el inbox una sola vez suele ser demasiado optimista. El correo es asíncrono, así que prefiero polling con pausa corta y un límite total. Cada intento debe tener una condición clara:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;wait_for_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;inbox&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TestInbox&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;20.0&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;deadline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;deadline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;list_messages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;inbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;inbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run_id&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt;
        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;TimeoutError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;No llegó el email para &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;inbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El &lt;code&gt;timeout&lt;/code&gt; debe representar el contrato del entorno de pruebas, no una espera infinita disfrazada. Si el proveedor ofrece webhook, puede sustituir al polling; la misma correlación sigue siendo necesaria.&lt;/p&gt;

&lt;p&gt;Limpio en un bloque &lt;code&gt;finally&lt;/code&gt;, incluso cuando falla la aserción. Si la limpieza falla, la registro como una alerta de infraestructura sin ocultar el error principal del test. Y si el sistema bajo prueba necesita reintentos, el test debe verificar que usa el mismo intento lógico, no aceptar cualquier mensaje que aparezca.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qué guardar como evidencia en CI
&lt;/h2&gt;

&lt;p&gt;Una evidencia útil es pequeña y suficiente:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;run_id&lt;/code&gt; de la ejecución;&lt;/li&gt;
&lt;li&gt;dirección de prueba, parcialmente enmascarada;&lt;/li&gt;
&lt;li&gt;timestamp de creación y de recepción;&lt;/li&gt;
&lt;li&gt;identificador del mensaje;&lt;/li&gt;
&lt;li&gt;estado final: recibido, timeout o limpieza fallida.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No guardes el HTML completo por defecto. Puede contener tokens de recuperación o datos personales de una prueba. Un snapshot del estado y el mensaje de error suele bastar para reproducir el problema. Si el equipo necesita ver la UI, aplica el mismo criterio de aislamiento a cada sesión; &lt;a href="https://dev.to/silviutech/react-errores-inline-sin-mover-el-formulario-b84"&gt;mostrar errores inline sin mover el formulario&lt;/a&gt; también depende de que los datos de prueba no se pisen entre sí.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preguntas frecuentes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ¿Puedo usar una sola dirección para todos los tests?
&lt;/h3&gt;

&lt;p&gt;Solo para una prueba local muy controlada. En CI, una dirección por ejecución reduce colisiones y hace el fallo más explicable.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Debo borrar el buzón después de cada test?
&lt;/h3&gt;

&lt;p&gt;Sí, o al menos marcarlo para una limpieza con TTL. El &lt;code&gt;finally&lt;/code&gt; es la red de seguridad cuando una aserción se rompe.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Qué hago si el email tarda más de lo esperado?
&lt;/h3&gt;

&lt;p&gt;Registra el timeout y los metadatos de entrega. Luego revisa proveedor, cola y endpoint por separado. Aumentar el timeout sin evidencia solo vuelve el pipeline más lento.&lt;/p&gt;

&lt;p&gt;El patrón no requiere una arquitectura grande: identidad, correlación, espera limitada y limpieza. Con esas piezas, una prueba de email en FastAPI deja de depender de mensajes casuales en un buzón compartido y la automatización se vuelve bastante más tranquila.&lt;/p&gt;

</description>
      <category>python</category>
      <category>fastapi</category>
      <category>testing</category>
      <category>automation</category>
    </item>
    <item>
      <title>FastAPI: emails de prueba con un buzón por ejecución</title>
      <dc:creator>Olivia Cheng</dc:creator>
      <pubDate>Fri, 02 Oct 2026 11:23:25 +0000</pubDate>
      <link>https://dev.to/oliviachen7/fastapi-emails-de-prueba-con-un-buzon-por-ejecucion-m9e</link>
      <guid>https://dev.to/oliviachen7/fastapi-emails-de-prueba-con-un-buzon-por-ejecucion-m9e</guid>
      <description>&lt;p&gt;Cuando una API envía un email de verificación, el test suele parecer sencillo: crear usuario, esperar el mensaje y abrir el enlace. El problema aparece cuando varios tests comparten el mismo buzón. Un mensaje viejo puede pasar por nuevo, un reintento puede leer el token de otra ejecución y el fallo termina siendo dificil de reproducir.&lt;/p&gt;

&lt;p&gt;En proyectos con FastAPI he encontrado una solución muy práctica: cada ejecución debe tener su propio buzón lógico, su propio identificador de correlación y una fecha clara de expiración. No hace falta convertir el test en una plataforma enorme. Hace falta que sus datos no se mezclen.&lt;/p&gt;

&lt;h2&gt;
  
  
  El problema de compartir un buzón de prueba
&lt;/h2&gt;

&lt;p&gt;Un buzón compartido parece cómodo al principio. El equipo configura una dirección, añade una función para leer el último mensaje y todos los tests avanzan. Pero “el último” no es una propiedad estable cuando hay paralelismo.&lt;/p&gt;

&lt;p&gt;Los fallos más comunes son estos:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Un test encuentra un email de la ejecución anterior.&lt;/li&gt;
&lt;li&gt;Dos usuarios de prueba reciben mensajes y el filtro solo mira el asunto.&lt;/li&gt;
&lt;li&gt;Un reintento consume el mismo token que el primer intento.&lt;/li&gt;
&lt;li&gt;El CI guarda el cuerpo completo del email en un artefacto.&lt;/li&gt;
&lt;li&gt;La limpieza depende de que el test termine sin errores.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Incluso una búsqueda escrita como &lt;strong&gt;dummy e mail&lt;/strong&gt; puede llegar a los logs de soporte. Por eso conviene que la fixture sea legible para una persona, pero que no dependa de interpretar texto libre para saber a qué ejecución pertenece.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qué debe garantizar cada ejecución
&lt;/h2&gt;

&lt;p&gt;Mi contrato mínimo tiene cuatro piezas:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Identidad única.&lt;/strong&gt; Genera un &lt;code&gt;run_id&lt;/code&gt; y úsalo en la dirección o en el identificador del buzón.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lectura acotada.&lt;/strong&gt; Busca mensajes después de la hora de inicio, con un destinatario exacto y un asunto conocido.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consumo idempotente.&lt;/strong&gt; Leer dos veces el mismo mensaje no debe cambiar el resultado del test.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expiración.&lt;/strong&gt; El buzón y sus mensajes deben poder eliminarse aunque la aserción falle.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;La dirección no tiene que ser bonita. Algo como &lt;code&gt;qa+run-8f31@example.test&lt;/code&gt; comunica más que una dirección fija. También hace más fácil relacionar una petición de FastAPI con el mensaje que la API generó.&lt;/p&gt;

&lt;p&gt;Esto se parece a medir señales útiles en un onboarding SaaS: no necesitas guardar todo para entender qué pasó. Necesitas escoger señales que permitan reconstruir la secuencia.&lt;/p&gt;

&lt;h2&gt;
  
  
  Un fixture sencillo para FastAPI
&lt;/h2&gt;

&lt;p&gt;El siguiente ejemplo muestra la forma del contrato. &lt;code&gt;InboxClient&lt;/code&gt; representa el proveedor de buzones de prueba y puede ser un fake local durante los tests unitarios.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;uuid4&lt;/span&gt;


&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;EmailFixture&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;

    &lt;span class="nd"&gt;@classmethod&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;inbox_client&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;EmailFixture&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;run_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;uuid4&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nb"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;address&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;inbox_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_inbox&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fastapi-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;wait_for_verification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;inbox_client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;inbox_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;recipient&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Verifica tu cuenta&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;inbox_client&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;inbox_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete_inbox&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;En un test de FastAPI, el cierre debe estar en un bloque &lt;code&gt;finally&lt;/code&gt;, no después de la última aserción. Si la aserción falla, el código posterior no se ejecuta. Es un detalle pequeño, pero suele ser la diferencia entre un CI limpio y una colección de buzones abandonados.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;fixture&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;EmailFixture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inbox_client&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/signup&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;fixture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;password&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;example-only&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;201&lt;/span&gt;

    &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;fixture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait_for_verification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inbox_client&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/verify?token=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;
&lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;fixture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inbox_client&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El timeout también es parte del diseño. Si cada test espera indefinidamente, una incidencia de entrega puede congelar toda la automatización. Devuelve un error con el &lt;code&gt;run_id&lt;/code&gt;, el destinatario y el último estado conocido, pero no con el token completo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automatizar la limpieza y la evidencia
&lt;/h2&gt;

&lt;p&gt;La limpieza debe tener dos capas. Primero, el test intenta borrar su buzón. Segundo, una tarea periódica elimina recursos cuyo &lt;code&gt;expires_at&lt;/code&gt; ya pasó. Así, un proceso cancelado no deja datos para siempre.&lt;/p&gt;

&lt;p&gt;Para depurar, conserva solo evidencia útil: &lt;code&gt;run_id&lt;/code&gt;, estado HTTP, identificador del mensaje, timestamps y una versión redactada del asunto. No guardes enlaces de verificación completos en los logs del CI. Esa información hace que un fallo de prueba sea un riesgo innecesario.&lt;/p&gt;

&lt;p&gt;También ayuda mostrar el estado al desarrollador. Una interfaz con &lt;a href="https://dev.to/silviutech/react-estados-async-que-no-rompen-la-accesibilidad"&gt;una interfaz React que mantiene el contexto&lt;/a&gt; puede enseñar “email solicitado”, “mensaje recibido” y “token consumido” sin saltos confusos. El backend sigue siendo la fuente de verdad, pero la UI no deberia esconder el estado real.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist final
&lt;/h2&gt;

&lt;p&gt;Antes de dar por terminada la fixture, compruebo lo siguiente:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cada ejecución crea una identidad de buzón diferente.&lt;/li&gt;
&lt;li&gt;El filtro combina destinatario, asunto y hora de inicio.&lt;/li&gt;
&lt;li&gt;Un mensaje no se consume dos veces por accidente.&lt;/li&gt;
&lt;li&gt;El &lt;code&gt;finally&lt;/code&gt; elimina el buzón incluso si una aserción falla.&lt;/li&gt;
&lt;li&gt;Existe una limpieza de respaldo por expiración.&lt;/li&gt;
&lt;li&gt;Los logs no contienen tokens completos ni cuerpos innecesarios.&lt;/li&gt;
&lt;li&gt;El timeout produce un diagnóstico accionable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Con este contrato, las pruebas de email dejan de depender de la suerte. La API puede reintentarse, el CI puede ejecutar varios casos en paralelo y el equipo puede leer un fallo sin adivinar qué buzón pertenecia a qué ejecución. Es una mejora pequeña, pero se nota rápido cuando el proyecto crece.&lt;/p&gt;

</description>
      <category>python</category>
      <category>fastapi</category>
      <category>testing</category>
      <category>automation</category>
    </item>
    <item>
      <title>FastAPI: fixtures de email para tests reproducibles</title>
      <dc:creator>Olivia Cheng</dc:creator>
      <pubDate>Tue, 29 Sep 2026 07:09:53 +0000</pubDate>
      <link>https://dev.to/oliviachen7/fastapi-fixtures-de-email-para-tests-reproducibles-3nop</link>
      <guid>https://dev.to/oliviachen7/fastapi-fixtures-de-email-para-tests-reproducibles-3nop</guid>
      <description>&lt;p&gt;Los flujos de email son una prueba pequeña que suele esconder muchos problemas. Un registro puede crear un usuario, enviar un mensaje, consumir un token y cambiar el estado de una cuenta. Si cada test depende de un buzón compartido o de un correo que llega “cuando puede”, el resultado deja de ser reproducible.&lt;/p&gt;

&lt;p&gt;En proyectos Python prefiero tratar el correo de prueba como una fixture con contrato propio. La aplicación no tiene que saber si detrás existe un fake local, un servicio de integración o un correo desechable gratis para una comprobación manual. Solo necesita recibir una interfaz predecible, consultar un mensaje y limpiar el recurso al terminar.&lt;/p&gt;

&lt;h2&gt;
  
  
  El problema: una fixture no es un buzón compartido
&lt;/h2&gt;

&lt;p&gt;Un buzón compartido parece práctico al principio. Pero dos ejecuciones pueden usar el mismo asunto, una lectura puede consumir el mensaje de otra prueba y un reintento puede encontrar un token viejo. Despues aparecen fallos intermitentes que no se pueden reproducir en el portátil.&lt;/p&gt;

&lt;p&gt;La unidad correcta es una fixture por escenario. Cada una debe tener un identificador, un propósito, una fecha de expiración y una regla de limpieza. Para un test de registro, por ejemplo, el flujo podría ser:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;crear fixture -&amp;gt; ejecutar signup -&amp;gt; esperar mensaje -&amp;gt; validar token -&amp;gt; eliminar fixture
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El test no debería buscar “el último email”. Debería pedir el mensaje asociado a su &lt;code&gt;fixture_id&lt;/code&gt; y verificar que el destinatario, el asunto y el token pertenecen a ese escenario. Es un cambio pequeño, pero elimina bastante ruido.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define el contrato de la fixture
&lt;/h2&gt;

&lt;p&gt;Antes de escribir el endpoint, define qué necesita el test. Un contrato mínimo puede tener estas operaciones:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;

&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MailFixture&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;fixture_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;expires_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MailFixtureStore&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;purpose&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;MailFixture&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;wait_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fixture_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout_seconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fixture_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La función &lt;code&gt;wait_for&lt;/code&gt; debe terminar con un estado claro: mensaje recibido, timeout o fixture expirada. No conviene devolver una cadena vacía para los tres casos. Un error explícito permite que CI distinga entre un producto que no envió el correo y un entorno que tardó demasiado.&lt;/p&gt;

&lt;p&gt;También conviene guardar una huella del caso, no el contenido completo del mensaje en cada log. &lt;code&gt;fixture_id&lt;/code&gt;, &lt;code&gt;test_name&lt;/code&gt;, &lt;code&gt;message_id&lt;/code&gt; y duración suelen ser suficiente para investigar. Si necesitas revisar la interfaz, aquí ayuda &lt;a href="https://dev.to/silviutech/react-feedback-estable-al-enviar-emails-3gda"&gt;feedback estable al enviar emails&lt;/a&gt;, porque el estado visible debe explicar si el test está esperando, ha terminado o ha fallado.&lt;/p&gt;

&lt;h2&gt;
  
  
  Un adaptador pequeño para FastAPI
&lt;/h2&gt;

&lt;p&gt;FastAPI puede exponer la fixture solo en un entorno de pruebas. La dependencia mantiene el endpoint limpio y permite sustituir la implementación en un test unitario:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;APIRouter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HTTPException&lt;/span&gt;

&lt;span class="n"&gt;router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;APIRouter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_fixture_store&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;MailFixtureStore&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;MailFixtureStore&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nd"&gt;@router.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/test-fixtures/mail&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_mail_fixture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;purpose&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MailFixtureStore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get_fixture_store&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;purpose&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Falta purpose&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;purpose&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;purpose&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;En producción, este endpoint debe estar desactivado o protegido por una frontera de entorno. Una variable como &lt;code&gt;ENABLE_TEST_FIXTURES=false&lt;/code&gt; no reemplaza la autenticación, pero sí evita que una ruta de soporte se publique por accidente. Las APIs que crean datos efímeros deben devolver también &lt;code&gt;expires_at&lt;/code&gt;, para que el cliente no dependa de una suposición escondida.&lt;/p&gt;

&lt;p&gt;Para tests de integración, una implementación HTTP puede hablar con el proveedor elegido. Para tests unitarios, un fake en memoria es más rápido y suficiente. Lo importante es que las dos implementaciones respeten el mismo contrato. Si el producto tiene correos de trial, merece la pena &lt;a href="https://dev.to/hannahdev56/emails-de-trial-sin-contaminar-tu-embudo-2gon"&gt;usar emails de trial sin contaminar el embudo&lt;/a&gt; y mantener esos escenarios separados de los datos de analítica.&lt;/p&gt;

&lt;h2&gt;
  
  
  Aislamiento, limpieza y diagnóstico
&lt;/h2&gt;

&lt;p&gt;El aislamiento no termina cuando llega el token. Cada test debe usar un identificador único, y la limpieza debe ocurrir aunque una aserción falle. En pytest, una fixture async con &lt;code&gt;yield&lt;/code&gt; expresa bien esa responsabilidad:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;

&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;mail_fixture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;fixture&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;purpose&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;signup&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;fixture&lt;/span&gt;
    &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fixture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fixture_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Define un timeout corto para polling y un deadline total para el escenario. Un intervalo de dos segundos puede ser correcto en una integración remota, pero resulta lento si se repite cientos de veces. Para reducir coste, usa backoff limitado y detén la consulta al detectar un estado terminal.&lt;/p&gt;

&lt;p&gt;Los nombres también importan. Un &lt;code&gt;purpose&lt;/code&gt; como &lt;code&gt;signup-ci-1842&lt;/code&gt; ayuda más que &lt;code&gt;test-email&lt;/code&gt;. Y aunque el sistema se llame tempail en una nota antigua, el identificador real debe ser consistente en el código, los logs y los reportes. No guardes tokens completos en los logs; basta con un hash corto o con los últimos caracteres cuando sea estrictamente necesario.&lt;/p&gt;

&lt;p&gt;Para una revisión manual aislada, &lt;a href="https://tempmailso.com" rel="noopener noreferrer"&gt;temp mail.so&lt;/a&gt; puede servir como referencia de un buzón temporal, pero no debe convertirse en una dependencia oculta de CI ni recibir información personal. El test automatizado necesita un proveedor controlable, una política de retención y una forma de borrar los datos.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preguntas rápidas
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ¿Debo usar un correo desechable gratis en todos los tests?
&lt;/h3&gt;

&lt;p&gt;No. Para la mayoría de los tests unitarios, un fake local es más rápido. Reserva una integración real para validar el transporte, las plantillas y el comportamiento del proveedor.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Conviene compartir una fixture entre varios tests?
&lt;/h3&gt;

&lt;p&gt;Solo si el escenario es explícitamente compartido. Como regla general, una fixture por test reduce el acoplamiento y hace que un fallo sea mas fácil de entender.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Qué hago con un timeout?
&lt;/h3&gt;

&lt;p&gt;Regístralo como un resultado distinto de “mensaje inválido”. Guarda el &lt;code&gt;fixture_id&lt;/code&gt;, el deadline y el estado del proveedor. Así sabrás si falló la aplicación, el transporte o el propio test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist final
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Cada test crea una fixture aislada y con expiración.&lt;/li&gt;
&lt;li&gt;El contrato diferencia recibido, expirado y timeout.&lt;/li&gt;
&lt;li&gt;El adaptador de FastAPI se puede sustituir por un fake.&lt;/li&gt;
&lt;li&gt;La limpieza corre incluso después de una aserción fallida.&lt;/li&gt;
&lt;li&gt;Los logs usan identificadores, no tokens ni contenido sensible.&lt;/li&gt;
&lt;li&gt;CI tiene un proveedor controlable y una ruta de diagnóstico.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Este diseño no necesita ser grande. Una interfaz pequeña, estados explícitos y una limpieza garantizada suelen ser suficiente para que los tests de email dejen de ser una fuente misteriosa de fallos. Y cuando el flujo crece, ya tendrás un contrato claro sobre el que añadir reintentos, métricas o nuevos proveedores.&lt;/p&gt;

</description>
      <category>python</category>
      <category>fastapi</category>
      <category>testing</category>
      <category>automation</category>
    </item>
  </channel>
</rss>
