<?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: un buzón de prueba que no se repite</title>
      <dc:creator>Olivia Cheng</dc:creator>
      <pubDate>Sat, 24 Oct 2026 23:22:48 +0000</pubDate>
      <link>https://dev.to/oliviachen7/fastapi-un-buzon-de-prueba-que-no-se-repite-22pf</link>
      <guid>https://dev.to/oliviachen7/fastapi-un-buzon-de-prueba-que-no-se-repite-22pf</guid>
      <description>&lt;p&gt;Un test de registro que pasa una vez y falla en la siguiente suele esconder el mismo problema: el buzón de prueba se está reutilizando. El mensaje anterior sigue ahí, el enlace ya caducó o dos ejecuciones están esperando el mismo correo. El resultado es una prueba frágil y dificil de diagnosticar.&lt;/p&gt;

&lt;p&gt;En proyectos de Python he encontrado más estable tratar cada correo como un recurso temporal, no como una constante global. La idea de &lt;em&gt;use and throw email&lt;/em&gt; encaja bien aquí: crear un buzón para una corrida, consumir solo el mensaje esperado y dejarlo fuera del siguiente intento. Es una forma sencilla de que la automatización sea más predecible.&lt;/p&gt;

&lt;h2&gt;
  
  
  El problema de reutilizar un email de prueba
&lt;/h2&gt;

&lt;p&gt;Imagina un endpoint que registra a un usuario y envía un enlace de confirmación. Un test puede usar &lt;code&gt;qa@example.test&lt;/code&gt; en cada ejecución, pero pronto aparecen varios problemas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Un mensaje viejo coincide con el asunto esperado.&lt;/li&gt;
&lt;li&gt;Dos jobs de CI leen el mismo correo casi al mismo tiempo.&lt;/li&gt;
&lt;li&gt;Un reintento crea otro usuario, pero conserva la identidad anterior.&lt;/li&gt;
&lt;li&gt;El diagnóstico no distingue un fallo de entrega de un fallo de lectura.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Un buzón temporal también puede tener nombres confusos en la documentación interna, como &lt;code&gt;temp org mail&lt;/code&gt; o &lt;code&gt;tempail&lt;/code&gt;. No importa el nombre comercial o coloquial: el contrato técnico debe decir quién crea el buzón, cuánto vive y qué mensaje puede consumir.&lt;/p&gt;

&lt;h2&gt;
  
  
  Un contrato pequeño para cada ejecución
&lt;/h2&gt;

&lt;p&gt;Antes de tocar FastAPI, define tres valores por corrida:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;run_id&lt;/code&gt;: identifica el job de CI o la ejecución local.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mailbox_id&lt;/code&gt;: identifica el buzón creado para esa corrida.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;expected_token&lt;/code&gt;: relaciona el enlace recibido con el usuario que se está probando.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;El &lt;code&gt;run_id&lt;/code&gt; evita que dos procesos compartan estado por accidente. El &lt;code&gt;mailbox_id&lt;/code&gt; permite limpiar el recurso al terminar. El token, por su parte, evita aceptar cualquier mensaje que tenga el mismo asunto. Puede parecer un poco de trabajo extra, pero ahorra horas cuando un test intermitente aparece solo en el pipeline.&lt;/p&gt;

&lt;p&gt;Si todavía no tienes una interfaz para aislar tus pruebas, este enfoque combina bien con &lt;a href="https://dev.to/oliviachen7/fastapi-aislar-pruebas-de-email-con-un-contrato-3mdg"&gt;aislar las pruebas de email con un contrato&lt;/a&gt;. La separación importante es entre el contrato del servicio y el proveedor concreto del buzón.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementación en FastAPI
&lt;/h2&gt;

&lt;p&gt;Una interfaz pequeña hace que el endpoint sea fácil de probar. No necesitas que FastAPI conozca todos los detalles del proveedor de correo:&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="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;FastAPI&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="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;Mailbox&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;mailbox_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_test_mailbox&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="n"&gt;Mailbox&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;mailbox_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="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="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="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&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;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="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;mailbox_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;@test.invalid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;mailbox_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;mailbox_id&lt;/span&gt;&lt;span class="p"&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-users&lt;/span&gt;&lt;span class="sh"&gt;"&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;create_test_user&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="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_test_mailbox&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="n"&gt;token&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="c1"&gt;# Guarda mailbox.mailbox_id y token junto al usuario creado.
&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;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;mailbox&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;mailbox_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;mailbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mailbox_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;verification_token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;token&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 una aplicación real, el endpoint no debería devolver el token. El ejemplo solo muestra la frontera entre la creación del usuario y el recurso de email. El adaptador de pruebas puede guardar el token esperado en el contexto del test y buscar un mensaje cuyo enlace lo contenga.&lt;/p&gt;

&lt;p&gt;También conviene no generar el &lt;code&gt;mailbox_id&lt;/code&gt; únicamente desde el email. Algunos proveedores normalizan direcciones y eso puede juntar dos buzones que parecian distintos. Guarda el identificador del proveedor como dato de primera clase.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cómo hacerlo seguro para reintentos
&lt;/h2&gt;

&lt;p&gt;La idempotencia empieza con una clave estable por intención. Por ejemplo, &lt;code&gt;signup:{run_id}&lt;/code&gt; puede ser la clave de la operación. Si el test reintenta después de un timeout, el servicio puede devolver el usuario ya creado en vez de enviar tres mensajes diferentes.&lt;/p&gt;

&lt;p&gt;Para leer el correo, usa una espera acotada y una condición precisa:&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="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;inbox&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token&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;30&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="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="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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;inbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;messages&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;token&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="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&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="mi"&gt;1&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;No llegó el email de verificación&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 código debe distinguir entre “no llegó” y “llegó un mensaje inválido”. El primero puede justificar un reintento de entrega; el segundo señala un problema de plantilla, token o aislamiento. Mezclar ambos estados hace que CI reintente sin necesidad y alarga el diagnóstico.&lt;/p&gt;

&lt;p&gt;Al terminar, elimina el buzón o márcalo como cerrado. Si el proveedor no permite borrado inmediato, aplica una política de expiración y no vuelvas a asignar ese identificador. La limpieza que se hace tarde es mejor que ninguna, pero una rutina &lt;code&gt;finally&lt;/code&gt; es mucho más confiable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist para CI
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Crea un buzón nuevo por job o por caso de prueba.&lt;/li&gt;
&lt;li&gt;Incluye &lt;code&gt;run_id&lt;/code&gt; y un token único en los datos de prueba.&lt;/li&gt;
&lt;li&gt;Busca el token esperado, no solo el asunto del mensaje.&lt;/li&gt;
&lt;li&gt;Define un timeout y registra la última fase conocida.&lt;/li&gt;
&lt;li&gt;Separa timeout de entrega y contenido inválido.&lt;/li&gt;
&lt;li&gt;Limpia el buzón en &lt;code&gt;finally&lt;/code&gt;, incluso cuando falla la aserción.&lt;/li&gt;
&lt;li&gt;Evita escribir direcciones completas y contenido del correo en logs públicos.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Hay un detalle que suele olvidarse: el cleanup también debe ser tolerante a reintentos. Si el buzón ya expiró, la limpieza debe considerarse correcta. De lo contrario, el test verde termina marcado como fallido por una operación secundaria.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusión
&lt;/h2&gt;

&lt;p&gt;Un email de prueba aislado no arregla por sí solo un flujo de autenticación, pero establece una frontera clara. FastAPI puede encargarse del endpoint, mientras un adaptador de testing administra el buzón y la lectura del mensaje. Con &lt;code&gt;run_id&lt;/code&gt;, tokens, timeouts y cleanup, cada ejecución sabe qué creó y qué debe consumir.&lt;/p&gt;

&lt;p&gt;El resultado es menos ruido en CI y una señal más útil cuando algo rompe. No hace falta una arquitectura enorme: basta con dejar de tratar el buzón de prueba como una variable fija y convertirlo en un recurso con dueño, ciclo de vida y contrato.&lt;/p&gt;

</description>
      <category>python</category>
      <category>fastapi</category>
      <category>testing</category>
      <category>automation</category>
    </item>
    <item>
      <title>FastAPI: idempotencia en emails de prueba</title>
      <dc:creator>Olivia Cheng</dc:creator>
      <pubDate>Tue, 20 Oct 2026 05:23:01 +0000</pubDate>
      <link>https://dev.to/oliviachen7/fastapi-idempotencia-en-emails-de-prueba-5858</link>
      <guid>https://dev.to/oliviachen7/fastapi-idempotencia-en-emails-de-prueba-5858</guid>
      <description>&lt;p&gt;Las pruebas de registro que dependen de un email suelen fallar de una forma dificil de explicar. El test crea un usuario, solicita un mensaje de verificación y espera una señal. Si la consulta tarda, el runner reintenta. Entonces aparecen dos usuarios, dos mensajes o un resultado que ya no sabemos a qué ejecución pertenece.&lt;/p&gt;

&lt;p&gt;En mis scripts de backend, trato cada email de prueba como una operación idempotente. La idea es simple: repetir la misma petición debe devolver el mismo resultado lógico, aunque el proveedor tarde o la automatización pierda una respuesta. Esto hace que un &lt;a href="https://dev.to/silviutech/fastapi-valida-emails-de-prueba-sin-ruido-54gb"&gt;endpoint para validar emails de prueba sin ruido&lt;/a&gt; sea mucho más util en CI.&lt;/p&gt;

&lt;h2&gt;
  
  
  El problema de repetir una prueba de email
&lt;/h2&gt;

&lt;p&gt;Imagina este flujo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;crear usuario -&amp;gt; pedir verificación -&amp;gt; revisar buzón -&amp;gt; confirmar usuario
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Un timeout entre “pedir verificación” y “revisar buzón” no significa que la primera operación no ocurrió. Sin un identificador estable, el test no puede distinguir entre una petición pendiente y una nueva petición. La reaccion habitual es repetir todo el flujo, y ahi nace el duplicado.&lt;/p&gt;

&lt;p&gt;Hay dos claves para evitarlo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;El cliente envía una clave de idempotencia por ejecución.&lt;/li&gt;
&lt;li&gt;El servidor guarda el estado y el resultado asociado a esa clave.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;La clave no debe ser el email solamente. Un mismo buzón puede servir para varias ejecuciones y una prueba nueva necesita su propio contexto. Una combinación como &lt;code&gt;run_id + caso_de_prueba&lt;/code&gt; suele ser suficiente para empezar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Un contrato idempotente para el endpoint
&lt;/h2&gt;

&lt;p&gt;El endpoint puede aceptar una petición pequeña y explicita:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"qa-42@example.test"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"test_case"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"signup_verification"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"idempotency_key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"run-2026-10-20-42"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Para la misma &lt;code&gt;idempotency_key&lt;/code&gt;, la API debería devolver uno de estos resultados:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;201 created&lt;/code&gt; si acaba de registrar la solicitud.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;200 existing&lt;/code&gt; si la solicitud ya estaba completa.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;202 pending&lt;/code&gt; si sigue en proceso y todavía no hay resultado final.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;409 conflict&lt;/code&gt; si la clave se reutilizó con un email o caso diferente.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;El &lt;code&gt;409&lt;/code&gt; es importante. Si una clave cambia de significado en mitad de una prueba, ocultar el problema detrás de un &lt;code&gt;200&lt;/code&gt; produce diagnósticos confusos. Un mensaje de error claro ahorra bastante tiempo despues.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementación pequeña con FastAPI
&lt;/h2&gt;

&lt;p&gt;Para una prueba local no hace falta construir un sistema enorme. Un repositorio en memoria permite probar el contrato antes de elegir PostgreSQL o Redis:&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;FastAPI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HTTPException&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pydantic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;EmailStr&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="n"&gt;requests&lt;/span&gt;&lt;span class="p"&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;dict&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&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;VerificationRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;EmailStr&lt;/span&gt;
    &lt;span class="n"&gt;test_case&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;idempotency_key&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;@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;/verification-requests&lt;/span&gt;&lt;span class="sh"&gt;"&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;create_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;VerificationRequest&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;saved&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;idempotency_key&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;saved&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;same_input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;saved&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="o"&gt;==&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;saved&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_case&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;test_case&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;same_input&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;409&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;Clave reutilizada&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="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&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;existing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;saved&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;record&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="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&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_case&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;test_case&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&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;pending&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="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;idempotency_key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;record&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;status&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;created&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;record&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, la escritura debe tener una restricción única sobre &lt;code&gt;idempotency_key&lt;/code&gt;. El patrón de “buscar y luego insertar” puede sufrir una carrera cuando dos workers reciben la misma petición al mismo tiempo. La base de datos tiene que ser la ultima defensa, no sólo el código Python.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cómo conectar la automatización sin duplicados
&lt;/h2&gt;

&lt;p&gt;La automatización puede separar tres pasos: reservar la solicitud, esperar la señal y confirmar el resultado. Cada paso consulta el estado existente antes de hacer trabajo nuevo.&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="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;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&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&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="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;request&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;get&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;/verification-requests/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;key&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;if&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;confirmed&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="n"&gt;request&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;message&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;find_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;key&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;message&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;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;confirm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;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;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="mi"&gt;1&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;status&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;pending&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;key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El worker no deberia crear otra solicitud al recibir &lt;code&gt;pending&lt;/code&gt;. Puede guardar el último momento de consulta y devolver el control al runner. En un sistema que necesita un correo temporal desechable para cada caso, conviene &lt;a href="https://dev.to/silviutech/react-formularios-async-que-respetan-el-foco-ccd"&gt;usar formularios async que respetan el foco&lt;/a&gt; en la interfaz de pruebas, pero la misma regla aplica: una ejecución, una clave, un estado observable.&lt;/p&gt;

&lt;p&gt;Si el equipo necesita &lt;strong&gt;generar un correo desechable&lt;/strong&gt;, esa acción debe quedar fuera de la clave de idempotencia o asociarse a ella de forma explícita. De lo contrario, un reintento puede reservar otro buzón y perder el mensaje original. En mis notas he visto nombres como &lt;code&gt;temp mailid&lt;/code&gt; y &lt;code&gt;temp org mail&lt;/code&gt;; aunque parezcan equivalentes, conviene conservar el identificador que realmente devolvió el adaptador.&lt;/p&gt;

&lt;p&gt;También es útil registrar &lt;code&gt;created_at&lt;/code&gt;, &lt;code&gt;expires_at&lt;/code&gt;, &lt;code&gt;message_id&lt;/code&gt; y un código de error del proveedor. No guardaría el contenido completo del mensaje si sólo se necesita verificar un token. Menos datos hacen el debugging mas seguro y reducen el ruido en los logs.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  ¿Una clave de idempotencia debe durar para siempre?
&lt;/h3&gt;

&lt;p&gt;No. Debe durar lo suficiente para cubrir los reintentos de la ejecución. Un TTL evita que el almacén crezca sin limite y permite reutilizar nombres de pruebas antiguas con un nuevo &lt;code&gt;run_id&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Qué hacemos si el mensaje llega después del timeout?
&lt;/h3&gt;

&lt;p&gt;Marca el caso como &lt;code&gt;pending&lt;/code&gt; o &lt;code&gt;expired&lt;/code&gt;, pero conserva la evidencia. Un proceso de limpieza puede retirar el buzón y el runner puede decidir si reintenta toda la prueba. No conviertas un timeout en “email inexistente” sin comprobar el estado del proveedor.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Esto sustituye una prueba end-to-end?
&lt;/h3&gt;

&lt;p&gt;No. Este patrón mejora la confiabilidad del flujo y hace sus fallos más legibles. Todavía conviene tener algunas pruebas end-to-end con un proveedor real y otras unitarias con un adaptador falso.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist antes de llevarlo a CI
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Cada ejecución genera un &lt;code&gt;run_id&lt;/code&gt; único.&lt;/li&gt;
&lt;li&gt;[ ] La clave de idempotencia combina ejecución y caso de prueba.&lt;/li&gt;
&lt;li&gt;[ ] Reutilizar la clave con datos distintos devuelve &lt;code&gt;409&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;[ ] La base de datos protege la inserción concurrente.&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;pending&lt;/code&gt;, &lt;code&gt;expired&lt;/code&gt; y &lt;code&gt;provider_error&lt;/code&gt; son estados distintos.&lt;/li&gt;
&lt;li&gt;[ ] El token o contenido sensible no aparece en logs.&lt;/li&gt;
&lt;li&gt;[ ] El cleanup conoce la caducidad del buzón y de la solicitud.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No hace falta añadir mucha infraestructura para obtener el beneficio. Con un contrato pequeño, estados claros y reintentos que consultan antes de crear, las pruebas de email dejan de depender de la suerte. La automatización se vuelve mas predecible y el diagnóstico, bastante mas rapido.&lt;/p&gt;

</description>
      <category>python</category>
      <category>fastapi</category>
      <category>testing</category>
      <category>automation</category>
    </item>
    <item>
      <title>FastAPI: contrato de espera para emails de prueba</title>
      <dc:creator>Olivia Cheng</dc:creator>
      <pubDate>Mon, 19 Oct 2026 23:23:13 +0000</pubDate>
      <link>https://dev.to/oliviachen7/fastapi-contrato-de-espera-para-emails-de-prueba-1fci</link>
      <guid>https://dev.to/oliviachen7/fastapi-contrato-de-espera-para-emails-de-prueba-1fci</guid>
      <description>&lt;p&gt;Probar un registro con confirmación por email suele empezar de forma simple: llamar al endpoint, abrir un buzón y esperar a que llegue un mensaje. El problema aparece cuando el test falla. ¿La aplicación no creó el email, el proveedor tardó, la fixture fue compartida o el enlace era incorrecto?&lt;/p&gt;

&lt;p&gt;En proyectos con FastAPI, una solución práctica es definir un contrato de eventos antes de hacer polling. Así el test no espera “un email cualquiera”, sino una señal concreta que puede correlacionarse, expirar y explicarse en CI. La idea también funciona cuando usas un correo temporal o una dirección de correo desechable para aislar pruebas.&lt;/p&gt;

&lt;h2&gt;
  
  
  El problema de esperar un email sin contrato
&lt;/h2&gt;

&lt;p&gt;Un &lt;code&gt;sleep(10)&lt;/code&gt; no describe qué está ocurriendo. Solo aplaza el diagnóstico y hace que cada ejecución lenta parezca un fallo de entrega. En una suite con varios workers, además, una fixture puede quedar disponible demasiado pronto y otro test la toma. Eso hace los logs se vuelva dificil de leer cuando dos problemas ocurren juntos.&lt;/p&gt;

&lt;p&gt;Antes de consultar el buzón, conviene registrar al menos:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;run_id&lt;/code&gt;: identifica esta ejecución y su worker.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;fixture_id&lt;/code&gt;: identifica el buzón reservado.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;expected_event&lt;/code&gt;: el evento que debería crear el email.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;expected_subject&lt;/code&gt;: una comprobación rápida de intención.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;deadline&lt;/code&gt;: el momento máximo de espera.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Este contexto convierte un timeout en una pregunta concreta. También ayuda a separar un error de negocio de un problema en la entrega. En una prueba de rotación de secretos por correo, por ejemplo, no basta con saber que llegó un mensaje: hay que demostrar que pertenece al secreto y a la ejecución correctos.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define el evento antes de llamar al endpoint
&lt;/h2&gt;

&lt;p&gt;El contrato debe existir antes de enviar el formulario o crear el usuario. Un ejemplo pequeño podría ser:&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;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="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timezone&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;new_email_expectation&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="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;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&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;run_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;uuid4&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fixture_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;fixture_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expected_event&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;signup.verification.created&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;expected_subject&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;Confirma tu cuenta&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;deadline&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="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seconds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;isoformat&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;Después, el endpoint puede recibir el &lt;code&gt;run_id&lt;/code&gt; como un identificador de correlación. No hace falta guardar el token completo en el contrato. Un ID de evento o un digest del token es suficiente para comprobar que el mensaje observado corresponde al flujo, sin convertir los logs en una fuente de datos sensibles.&lt;/p&gt;

&lt;p&gt;Este patrón es parecido a trabajar con &lt;a href="https://dev.to/oliviachen7/fastapi-fixtures-de-email-que-expiran-3oin"&gt;fixtures de email que expiran en FastAPI&lt;/a&gt;, pero añade una frontera visible entre la reserva del buzón y la observación del mensaje.&lt;/p&gt;

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

&lt;p&gt;En la aplicación, el evento puede modelarse con Pydantic. La respuesta no necesita exponer contenido privado; solo el estado que la automatización 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;pydantic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseModel&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;VerificationCreated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;event_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;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;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;subject&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;@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;/internal/test-events/verification&lt;/span&gt;&lt;span class="sh"&gt;"&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;202&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;record_verification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;VerificationCreated&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="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;event_store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;model_dump&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;status&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;accepted&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;event_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;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event_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, este endpoint debe estar protegido y separado de las rutas públicas. Para pruebas de integración, lo importante es que el test pueda verificar la primera mitad del recorrido: la aplicación aceptó el evento. Luego puede consultar el buzón para validar la segunda mitad.&lt;/p&gt;

&lt;h2&gt;
  
  
  Haz que el polling explique su espera
&lt;/h2&gt;

&lt;p&gt;El poller debe tener una pausa limitada, un cursor y estados explícitos. Por ejemplo, puede devolver &lt;code&gt;pending&lt;/code&gt;, &lt;code&gt;matched&lt;/code&gt;, &lt;code&gt;wrong_message&lt;/code&gt; o &lt;code&gt;expired&lt;/code&gt;. No es lo mismo un buzón vacío que un mensaje de otra ejecución.&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="k"&gt;async&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;mailbox&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;poll_every&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&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;expected&lt;/span&gt;&lt;span class="p"&gt;.&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="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;messages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;after&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;messages&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;message&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;expected&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;continue&lt;/span&gt;
            &lt;span class="k"&gt;if&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;subject&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expected_subject&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;status&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;matched&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;message_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;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;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;status&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;wrong_message&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;message_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;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&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;asyncio&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="n"&gt;poll_every&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;status&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;expired&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;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;expected&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 código real, convierte &lt;code&gt;deadline&lt;/code&gt; a &lt;code&gt;datetime&lt;/code&gt; y valida el origen de cada mensaje. El ejemplo se centra en la decisión: el test sabe que hacer con cada estado. Si el proveedor está temporalmente lento, un reintento acotado puede tener sentido. Si el mensaje pertenece a otro &lt;code&gt;run_id&lt;/code&gt;, reintentar no arregla la propiedad de la fixture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Aísla la fixture y clasifica el fallo
&lt;/h2&gt;

&lt;p&gt;Reserva el buzón con un propietario y libera la fixture al terminar, incluso cuando el test falla. Las fixtures cruzadas puede crear resultados que parecen aleatorios, aunque el problema sea determinista. Un nombre descriptivo como &lt;code&gt;temp org mail&lt;/code&gt; puede aparecer en una prueba de entrada inválida, pero nunca debe aceptarse como dominio o identificador real.&lt;/p&gt;

&lt;p&gt;También conviene imprimir un resumen seguro:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;status=wrong_message run_id=ci-1842 fixture_id=mailbox-7f2
expected=signup.verification.created observed=password.reset
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Oculta direcciones completas, tokens y cuerpos de email. Para investigar, guarda un ID de mensaje o una referencia controlada. Una rutina de observabilidad como esta se complementa bien con una &lt;a href="https://dev.to/alexcarteruk/sre-prueba-rotacion-de-secretos-por-correo-19f2"&gt;prueba de rotación de secretos por correo&lt;/a&gt;, donde la identidad del evento importa tanto como su llegada.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist para CI
&lt;/h2&gt;

&lt;p&gt;Antes de dar por estable la prueba, comprueba que:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;la fixture tiene un propietario y una fecha de expiración;&lt;/li&gt;
&lt;li&gt;el contrato se crea antes de llamar al endpoint;&lt;/li&gt;
&lt;li&gt;cada evento lleva &lt;code&gt;run_id&lt;/code&gt; y, si es posible, &lt;code&gt;event_id&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;el polling usa cursor, deadline y estados claros;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;wrong_message&lt;/code&gt; no se trata como un timeout transitorio;&lt;/li&gt;
&lt;li&gt;los logs omiten tokens y contenido sensible;&lt;/li&gt;
&lt;li&gt;la limpieza ocurre en un bloque &lt;code&gt;finally&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;El objetivo no es esperar más tiempo. Es saber por que estamos esperando y qué límite tiene esa espera. Cuando la señal de email es explícita, FastAPI, el buzón y CI pueden fallar de una manera mucho más útil.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  ¿Un correo temporal hace fiable el test por sí solo?
&lt;/h3&gt;

&lt;p&gt;No. Aísla los datos, pero la fiabilidad viene del propietario, la correlación, el deadline y la limpieza. El buzón es una dependencia, no la estrategia completa.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Cuándo conviene reintentar?
&lt;/h3&gt;

&lt;p&gt;Solo ante estados posiblemente transitorios, como un timeout del proveedor. Un mensaje equivocado, una fixture ocupada o un evento ausente requieren diagnóstico, no más intentos ciegos.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Cuál es el cambio más pequeño?
&lt;/h3&gt;

&lt;p&gt;Añade &lt;code&gt;run_id&lt;/code&gt;, &lt;code&gt;fixture_id&lt;/code&gt; y &lt;code&gt;failure_class&lt;/code&gt; al resultado actual. Incluso ese cambio sencillo vuelve mucho más claro qué pasó y que debería revisar el siguiente desarrollador.&lt;/p&gt;

</description>
      <category>python</category>
      <category>fastapi</category>
      <category>testing</category>
      <category>automation</category>
    </item>
    <item>
      <title>FastAPI: un recibo claro para cada email de prueba</title>
      <dc:creator>Olivia Cheng</dc:creator>
      <pubDate>Wed, 14 Oct 2026 23:23:10 +0000</pubDate>
      <link>https://dev.to/oliviachen7/fastapi-un-recibo-claro-para-cada-email-de-prueba-1b36</link>
      <guid>https://dev.to/oliviachen7/fastapi-un-recibo-claro-para-cada-email-de-prueba-1b36</guid>
      <description>&lt;p&gt;Una prueba de registro no termina cuando el endpoint responde &lt;code&gt;202&lt;/code&gt;. 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.&lt;/p&gt;

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

&lt;p&gt;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 &lt;code&gt;tp mail so&lt;/code&gt; y &lt;code&gt;temp mail.so&lt;/code&gt;, además de errores escritos como &lt;code&gt;tem email&lt;/code&gt; o &lt;code&gt;tepm mail com&lt;/code&gt;. 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Por qué una prueba necesita un recibo
&lt;/h2&gt;

&lt;p&gt;Un &lt;code&gt;True&lt;/code&gt; en una aserción no cuenta toda la historia. Para una prueba de email suelen importar al menos cuatro hechos:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;qué ejecución creó el buzón o alias;&lt;/li&gt;
&lt;li&gt;cuándo se solicitó el mensaje;&lt;/li&gt;
&lt;li&gt;qué identificador o asunto se encontró;&lt;/li&gt;
&lt;li&gt;cuándo se consumió el código y con qué resultado.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Por eso conviene devolver un recibo pequeño desde la API. Un recibo permite guardar &lt;code&gt;run_id&lt;/code&gt;, &lt;code&gt;message_id&lt;/code&gt;, &lt;code&gt;status&lt;/code&gt; y marcas de tiempo, sin copiar el cuerpo del email a los logs de CI.&lt;/p&gt;

&lt;h2&gt;
  
  
  El contrato mínimo del recibo
&lt;/h2&gt;

&lt;p&gt;Antes de conectar un proveedor real, define un contrato que se pueda probar en memoria. Una respuesta útil podría tener esta forma:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"run_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"checkout-1842"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"msg-91"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"verified"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"requested_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-10-15T06:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"observed_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-10-15T06:00:04Z"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Los estados deben ser pocos y explícitos: &lt;code&gt;created&lt;/code&gt;, &lt;code&gt;waiting&lt;/code&gt;, &lt;code&gt;verified&lt;/code&gt;, &lt;code&gt;expired&lt;/code&gt; y &lt;code&gt;failed&lt;/code&gt; suelen bastar. Si el cliente recibe &lt;code&gt;waiting&lt;/code&gt;, puede continuar el polling; si recibe &lt;code&gt;expired&lt;/code&gt;, debe crear una nueva ejecución, no reintentar indefinidamente.&lt;/p&gt;

&lt;p&gt;El &lt;code&gt;run_id&lt;/code&gt; es la frontera principal. El adaptador de correo debe buscar mensajes posteriores a &lt;code&gt;requested_at&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;Este contrato complementa una estrategia para &lt;a href="https://dev.to/silviutech/fastapi-separa-emails-de-signup-por-escenario-4ncb"&gt;separar emails de signup por escenario&lt;/a&gt;: cada escenario tiene una identidad que se puede comprobar de forma independiente.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementación en FastAPI
&lt;/h2&gt;

&lt;p&gt;El endpoint puede delegar la entrega en un proveedor con una interfaz pequeña. Así, el test unitario no depende de una bandeja remota.&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;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="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&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;FastAPI&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pydantic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseModel&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DeliveryReceipt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&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="n"&gt;message_id&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="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;status&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;requested_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;
    &lt;span class="n"&gt;observed_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MailProvider&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;def&lt;/span&gt; &lt;span class="nf"&gt;request&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="n"&gt;DeliveryReceipt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FakeMailProvider&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;request&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="n"&gt;DeliveryReceipt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&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;DeliveryReceipt&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="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;waiting&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;requested_at&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&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="n"&gt;provider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FakeMailProvider&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;/email-checks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response_model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;DeliveryReceipt&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;create_email_check&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="n"&gt;DeliveryReceipt&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;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&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 un proveedor real, &lt;code&gt;request&lt;/code&gt; puede crear el alias y registrar la hora de inicio. Otro método, como &lt;code&gt;observe&lt;/code&gt;, buscaría el mensaje correcto y devolvería un nuevo recibo con &lt;code&gt;message_id&lt;/code&gt; y &lt;code&gt;observed_at&lt;/code&gt;. La validación del &lt;code&gt;run_id&lt;/code&gt; debe ocurrir antes de crear recursos; un identificador vacío hace el diagnostico mas confuso.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cómo filtrar y depurar en CI
&lt;/h2&gt;

&lt;p&gt;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 &lt;code&gt;message_not_seen&lt;/code&gt;, &lt;code&gt;message_wrong_run&lt;/code&gt; o &lt;code&gt;expired&lt;/code&gt;. Son nombres aburridos, pero explican mucho mejor un fallo.&lt;/p&gt;

&lt;p&gt;Para cada job de CI, guarda solo un artefacto con:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;run_id&lt;/code&gt; y nombre del escenario;&lt;/li&gt;
&lt;li&gt;estado final y número de intentos;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;requested_at&lt;/code&gt;, &lt;code&gt;observed_at&lt;/code&gt; y tiempo total;&lt;/li&gt;
&lt;li&gt;identificadores redactados del alias y del mensaje.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;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 &lt;code&gt;run_id&lt;/code&gt; solo cuando el contrato sea idempotente; de lo contrario, crea una ejecución nueva.&lt;/p&gt;

&lt;p&gt;Si el equipo necesita más contexto, los &lt;a href="https://dev.to/silviutech/fastapi-contratos-de-email-para-pruebas-repetibles-27cn"&gt;contratos de email para pruebas repetibles&lt;/a&gt; ayudan a convertir los criterios de entrega en comprobaciones concretas. Un &lt;a href="https://tempmailso.com" rel="noopener noreferrer"&gt;generador de direcciones de correo desechables&lt;/a&gt; puede servir para escenarios aislados, pero la prueba debe seguir controlando TTL, datos permitidos y limpieza; el proveedor no sustituye el contrato.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  ¿Debo guardar el contenido del email?
&lt;/h3&gt;

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

&lt;h3&gt;
  
  
  ¿Un recibo reemplaza la aserción del test?
&lt;/h3&gt;

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

&lt;h3&gt;
  
  
  ¿Cuánto tiempo debe esperar el polling?
&lt;/h3&gt;

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

&lt;p&gt;Un recibo de email no necesita una plataforma grande. Con un &lt;code&gt;run_id&lt;/code&gt;, 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.&lt;/p&gt;

</description>
      <category>python</category>
      <category>fastapi</category>
      <category>testing</category>
      <category>automation</category>
    </item>
    <item>
      <title>FastAPI: fixtures de email que expiran</title>
      <dc:creator>Olivia Cheng</dc:creator>
      <pubDate>Wed, 14 Oct 2026 11:23:09 +0000</pubDate>
      <link>https://dev.to/oliviachen7/fastapi-fixtures-de-email-que-expiran-3oin</link>
      <guid>https://dev.to/oliviachen7/fastapi-fixtures-de-email-que-expiran-3oin</guid>
      <description>&lt;p&gt;Cuando una prueba de signup necesita leer un código de verificación, es tentador crear una dirección y dejarla viva para siempre. Al principio funciona. Después el entorno de CI acumula buzones, los mensajes viejos se mezclan con los nuevos y una prueba que pasa en local falla de forma intermitente.&lt;/p&gt;

&lt;p&gt;En proyectos Python donde he automatizado flujos de registro, me ha servido pensar en el buzón como un fixture con fecha de vencimiento, no como un recurso permanente. La idea es sencilla: cada ejecución obtiene un identificador aislado, conoce su tiempo de vida y deja una evidencia pequeña cuando termina.&lt;/p&gt;

&lt;h2&gt;
  
  
  El problema de los fixtures de email eternos
&lt;/h2&gt;

&lt;p&gt;Un fixture de correo tiene tres riesgos frecuentes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Contaminación:&lt;/strong&gt; una ejecución puede leer el código generado por otra.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coste operativo:&lt;/strong&gt; los buzones que nadie elimina se vuelven basura dificil de rastrear, y nadie sabe bien quien deberia limpiarla.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Diagnóstico pobre:&lt;/strong&gt; cuando falla el pipeline, no sabes si el mensaje era nuevo o estaba esperando desde ayer.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Esto se parece a cualquier recurso temporal de backend: una sesión, un token o un archivo de trabajo. Si no hay un propietario y una expiración, el recurso acaba siendo compartido por accidente y el diagnostico se vuelve mas lento.&lt;/p&gt;

&lt;p&gt;Para flujos de CI conviene usar un correo temporal gratis solo en datos no sensibles y con límites claros. Nunca debe recibir información real de usuarios. También conviene normalizar entradas extrañas, como &lt;code&gt;temp gamil com&lt;/code&gt; o &lt;code&gt;temp mailid&lt;/code&gt;, porque aparecen en datos de prueba copiados a mano y pueden ocultar un bug de validación.&lt;/p&gt;

&lt;h2&gt;
  
  
  Un contrato pequeño para crear y expirar buzones
&lt;/h2&gt;

&lt;p&gt;Antes de escribir código, defino un contrato mínimo para el adaptador de email:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;create(run_id)&lt;/code&gt; devuelve un alias y un &lt;code&gt;expires_at&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;wait_for_code(alias, since)&lt;/code&gt; solo acepta mensajes posteriores al inicio de la prueba.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;delete(alias)&lt;/code&gt; es seguro si se llama dos veces.&lt;/li&gt;
&lt;li&gt;El adaptador nunca expone el contenido completo del mensaje en los logs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;El &lt;code&gt;run_id&lt;/code&gt; separa ejecuciones paralelas. El campo &lt;code&gt;since&lt;/code&gt; evita aceptar un código antiguo, incluso si el proveedor tarda en eliminar mensajes. Esta separación hace que el automatizador sea mucho más predecible y ayuda a revisar fallos sin guardar datos de más.&lt;/p&gt;

&lt;p&gt;Si el job se ejecuta con un cron, también ayuda usar una &lt;a href="https://dev.to/silviutech/llms-prompts-cortos-para-cron-fiables-5e34"&gt;estrategia de cron fiables con prompts cortos&lt;/a&gt;: la tarea debe tener un objetivo medible y una salida fácil de comprobar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementación mínima en FastAPI
&lt;/h2&gt;

&lt;p&gt;El siguiente ejemplo muestra la forma del contrato. El proveedor real queda detrás de una interfaz; así las pruebas unitarias no dependen de una bandeja externa.&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;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="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&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;FastAPI&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pydantic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseModel&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;BaseModel&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;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;MailboxProvider&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;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="n"&gt;Mailbox&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;...&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;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="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;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;InMemoryMailboxProvider&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;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="n"&gt;Mailbox&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;expires_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;minutes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&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;Mailbox&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="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;@test.invalid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expires_at&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;expires_at&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;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;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="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="k"&gt;pass&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="n"&gt;provider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;InMemoryMailboxProvider&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-mailboxes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response_model&lt;/span&gt;&lt;span class="o"&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;def&lt;/span&gt; &lt;span class="nf"&gt;create_test_mailbox&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="n"&gt;Mailbox&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;provider&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;En producción, el proveedor debe comprobar que &lt;code&gt;run_id&lt;/code&gt; tenga un formato permitido y aplicar un TTL del lado del servicio. El &lt;code&gt;test.invalid&lt;/code&gt; del ejemplo es deliberado: evita que una prueba envíe accidentalmente un correo a un dominio real. Es una regla simple, pero muy util.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cómo probar el flujo en CI
&lt;/h2&gt;

&lt;p&gt;Una prueba útil no solo revisa que exista una dirección. También comprueba el aislamiento y la limpieza:&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_mailbox_has_expiration&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="n"&gt;provider&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run-123&lt;/span&gt;&lt;span class="sh"&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;mailbox&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run-123@test.invalid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;assert&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;expires_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&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;test_parallel_runs_get_different_addresses&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;first&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;provider&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run-a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;second&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;provider&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run-b&lt;/span&gt;&lt;span class="sh"&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;first&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;second&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;address&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;En el pipeline, genera un &lt;code&gt;run_id&lt;/code&gt; distinto por job y guarda solo el alias, el estado y el tiempo de expiración. Si una prueba falla, un artefacto pequeño con esos tres campos suele ser suficiente, aunque a veces parece poco. En CI, los tiempos de espera cambia según el proveedor, asi que conviene medirlos. La limpieza debe estar en un bloque &lt;code&gt;finally&lt;/code&gt;, para que ocurra también tras un assertion fallido.&lt;/p&gt;

&lt;p&gt;Esto complementa las buenas prácticas para &lt;a href="https://dev.to/hannahdev56/como-probar-correos-de-onboarding-en-un-saas-sin-ensuciar-metricas-pjn"&gt;probar correos de onboarding sin ensuciar métricas&lt;/a&gt;: separar la evidencia de test de los datos que usa analítica evita conclusiones medio engañosas.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  ¿El TTL debe ser igual para todos los tests?
&lt;/h3&gt;

&lt;p&gt;No necesariamente. Diez minutos suele bastar para una prueba de integración normal, pero un flujo con varios workers puede necesitar más. Elige el menor valor que cubra la latencia esperada y deja el número configurable.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Puedo reutilizar un buzón para acelerar el pipeline?
&lt;/h3&gt;

&lt;p&gt;Solo dentro de una misma prueba y si filtras por &lt;code&gt;since&lt;/code&gt; y por un identificador único. Reutilizarlo entre jobs paralelos ahorra poco y complica mucho el diagnóstico.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Dónde guardo los mensajes fallidos?
&lt;/h3&gt;

&lt;p&gt;Guarda metadatos redactados, no el cuerpo completo. Un hash del asunto, el alias y una marca de tiempo suelen dar suficiente contexto para investigar. Los logs de CI se comparten con más personas de las que parece.&lt;/p&gt;

&lt;p&gt;El patrón no requiere una arquitectura grande: un contrato pequeño, un identificador por ejecución y una expiración explícita ya eliminan buena parte de la flakiness. En FastAPI, además, separar el proveedor facilita cambiar de un fake local a un servicio controlado cuando el proyecto crece.&lt;/p&gt;

</description>
      <category>python</category>
      <category>fastapi</category>
      <category>testing</category>
      <category>automation</category>
    </item>
    <item>
      <title>Python: pruebas de email con un contrato de evento</title>
      <dc:creator>Olivia Cheng</dc:creator>
      <pubDate>Mon, 12 Oct 2026 14:24:14 +0000</pubDate>
      <link>https://dev.to/oliviachen7/python-pruebas-de-email-con-un-contrato-de-evento-15ak</link>
      <guid>https://dev.to/oliviachen7/python-pruebas-de-email-con-un-contrato-de-evento-15ak</guid>
      <description>&lt;p&gt;Las pruebas de verificación por email suelen fallar por una razón poco visible: el equipo trata el correo como un mensaje, cuando en realidad es un evento distribuido. La aplicación dispara una acción, un proveedor entrega el mensaje y la prueba debe encontrarlo dentro de una ventana concreta.&lt;/p&gt;

&lt;p&gt;Cuando esa ventana no está escrita en ninguna parte, aparecen síntomas conocidos: el test pasa en local pero no en CI, el polling abre un mensaje viejo o dos workers consumen el mismo correo. No siempre es un bug de Python ni del proveedor. Muchas veces falta un contrato pequeño que diga qué ocurrió, cuándo ocurrió y qué mensaje pertenece a esa ejecución.&lt;/p&gt;

&lt;p&gt;En este artículo propongo ese contrato para pruebas con Python y APIs. La idea sirve tanto si usas un &lt;a href="https://tempmailso.com" rel="noopener noreferrer"&gt;generador de correo temporal&lt;/a&gt; como si tienes un buzón de pruebas interno. El servicio solo debe entregar eventos identificables; la prueba decide si el evento es suyo.&lt;/p&gt;

&lt;h2&gt;
  
  
  El problema: un email no es solo un mensaje
&lt;/h2&gt;

&lt;p&gt;Imagina una prueba que crea una cuenta, solicita un código y espera el último correo recibido. El enfoque parece suficiente hasta que una ejecución anterior deja un mensaje en el buzón. Si el asunto coincide, la prueba puede recoger el token equivocado y fallar mucho después, cuando intenta usarlo.&lt;/p&gt;

&lt;p&gt;También sucede al reintentar. El primer intento quizá creó el usuario, pero perdió la respuesta por un timeout. El segundo intenta crear otra cuenta y recibe dos eventos. Sin una identidad de ejecución, el diagnóstico se vuelve una adivinanza un poco cara.&lt;/p&gt;

&lt;p&gt;Por eso me gusta modelar la entrega como un evento con cuatro datos mínimos:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;run_id&lt;/code&gt;: identifica la ejecución de la prueba.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;triggered_at&lt;/code&gt;: marca el instante en que se pidió el email.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;message_id&lt;/code&gt;: identifica el mensaje observado.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;consumed_at&lt;/code&gt;: indica cuándo la prueba aceptó el evento.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Un frontend puede seguir esta misma idea al &lt;a href="https://dev.to/silviutech/react-estados-de-email-que-si-se-pueden-probar-3n58"&gt;probar estados de email sin perder señales&lt;/a&gt;. La interfaz muestra estados; el backend necesita además una prueba de pertenencia.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qué debe contener el contrato de evento
&lt;/h2&gt;

&lt;p&gt;No hace falta guardar el contenido completo del email en todos los casos. Para una prueba normal, el contrato puede ser un diccionario serializable:&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;EmailEvent&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="n"&gt;message_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;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;received_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;
    &lt;span class="n"&gt;verification_url&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="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El &lt;code&gt;run_id&lt;/code&gt; debe viajar desde la prueba hasta el registro de la cuenta o el alias usado. Si el proveedor no permite filtros por metadatos, puedes incluir un fragmento seguro del identificador en el asunto o usar un buzón aislado por ejecución.&lt;/p&gt;

&lt;p&gt;Hay un detalle que se suele olvidar: &lt;code&gt;received_at&lt;/code&gt; no es lo mismo que &lt;code&gt;triggered_at&lt;/code&gt;. Un mensaje puede aparecer con retraso, por eso la regla correcta es aceptar &lt;code&gt;received_at &amp;gt;= triggered_at&lt;/code&gt; con una tolerancia explícita. Si no se documenta esa tolerancia, cada equipo la interpreta de una forma distinta y el test se vuelve inconsistantemente verde.&lt;/p&gt;

&lt;h2&gt;
  
  
  Polling con límites e idempotencia
&lt;/h2&gt;

&lt;p&gt;El polling debe tener una fecha de inicio, un intervalo y un límite total. Un bucle sin stop rule consume recursos y además oculta que el sistema no entregó el email. Este helper es deliberadamente simple:&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;collections.abc&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Callable&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;wait_for_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Callable&lt;/span&gt;&lt;span class="p"&gt;[[],&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;EmailEvent&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;timeout_s&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="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;interval_s&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="mi"&gt;1&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="n"&gt;EmailEvent&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_s&lt;/span&gt;
    &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;set&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;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;()&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;fetch&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;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message_id&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;
            &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message_id&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;event&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;return&lt;/span&gt; &lt;span class="n"&gt;event&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="n"&gt;interval_s&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 email event for run_id=&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La colección &lt;code&gt;seen&lt;/code&gt; hace que leer la misma página dos veces no cambie el resultado. Eso es idempotencia en una escala muy práctica. En producción de pruebas, también conviene registrar los &lt;code&gt;message_id&lt;/code&gt; rechazados y el número de intentos; esa información ayuda a ver si el filtro es demasiado estricto.&lt;/p&gt;

&lt;p&gt;Si un compañero busca “tamp mail com” en los apuntes, no es una estrategia de aislamiento: es una señal de que falta una fixture clara. Vale la pena corregir la fixture, no solo repetir la prueba.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cómo exponerlo en una API FastAPI
&lt;/h2&gt;

&lt;p&gt;Una API de pruebas puede guardar el contrato antes de iniciar una acción externa. El endpoint no debería esperar al email: devuelve el &lt;code&gt;run_id&lt;/code&gt; y otro proceso consulta el buzón.&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;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="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;FastAPI&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="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;/verification-runs&lt;/span&gt;&lt;span class="sh"&gt;"&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;create_verification_run&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;run_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;uuid4&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="c1"&gt;# Persistir run_id y triggered_at antes de enviar el email.
&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;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;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&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;waiting&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;Este patrón separa el envío de la lectura y hace más fácil observar cada etapa. Si tu flujo usa webhooks, también puedes comparar este diseño con &lt;a href="https://dev.to/silviutech/fastapi-webhooks-de-email-sin-carreras-2ea"&gt;webhooks de email en FastAPI sin carreras&lt;/a&gt;: en ambos casos, la clave es que una entrega tardía no cambie de dueño.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist para una prueba mantenible
&lt;/h2&gt;

&lt;p&gt;Antes de dar por estable una prueba, compruebo lo siguiente:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Cada ejecución tiene un &lt;code&gt;run_id&lt;/code&gt; único.&lt;/li&gt;
&lt;li&gt;El instante de disparo se guarda antes de la llamada externa.&lt;/li&gt;
&lt;li&gt;El filtro no usa únicamente el asunto del mensaje.&lt;/li&gt;
&lt;li&gt;El polling tiene timeout e intervalo visibles.&lt;/li&gt;
&lt;li&gt;Los mensajes observados quedan en el log de la ejecución.&lt;/li&gt;
&lt;li&gt;El test consume un evento una sola vez.&lt;/li&gt;
&lt;li&gt;El buzón o alias se elimina al terminar, incluso si la prueba falla.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No necesitas un sistema enorme para conseguirlo. Un contrato de evento bien nombrado suele quitar más flakiness que añadir reintentos al azar. Y cuando algo falla, el equipo puede responder qué pasó con datos, no con intuición.&lt;/p&gt;

</description>
      <category>python</category>
      <category>fastapi</category>
      <category>testing</category>
      <category>automation</category>
    </item>
    <item>
      <title>FastAPI: contratos para correos temporales en CI</title>
      <dc:creator>Olivia Cheng</dc:creator>
      <pubDate>Fri, 09 Oct 2026 05:23:46 +0000</pubDate>
      <link>https://dev.to/oliviachen7/fastapi-contratos-para-correos-temporales-en-ci-43j6</link>
      <guid>https://dev.to/oliviachen7/fastapi-contratos-para-correos-temporales-en-ci-43j6</guid>
      <description>&lt;p&gt;Una bandeja de correo temporal puede hacer que una prueba de integración sea rápida y repetible, pero solo si cada ejecución tiene límites claros. En esta guía veremos un contrato pequeño para FastAPI, pytest y CI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Por qué un correo temporal necesita un contrato
&lt;/h2&gt;

&lt;p&gt;En un flujo de registro, la aplicación crea un usuario, envía un código y espera que el test lo lea. El problema aparece cuando varias ejecuciones comparten el mismo buzón: un mensaje viejo parece válido, dos jobs leen el mismo código o una prueba falla solo cuando hay carga.&lt;/p&gt;

&lt;p&gt;La solución no es añadir &lt;code&gt;sleep(10)&lt;/code&gt; a todo. Es tratar el buzón como un recurso temporal, con un dueño, un identificador de ejecución y un momento de expiración. El contrato debería responder cuatro preguntas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;¿Qué job creó este buzón?&lt;/li&gt;
&lt;li&gt;¿Qué mensajes puede consumir?&lt;/li&gt;
&lt;li&gt;¿Cuánto tiempo puede esperar?&lt;/li&gt;
&lt;li&gt;¿Quién lo limpia si el test falla?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Este enfoque también ayuda a separar la aplicación de la infraestructura de correo. Una opción de &lt;a href="https://dev.to/silviutech/react-ayuda-de-email-sin-mover-el-layout-41ih"&gt;mantener el layout estable en React&lt;/a&gt; sirve para la interfaz; en backend, el equivalente es no mezclar la lógica del registro con los detalles del buzón.&lt;/p&gt;

&lt;h2&gt;
  
  
  El contrato mínimo para cada ejecución
&lt;/h2&gt;

&lt;p&gt;Para cada suite genero un alias o buzón con un nombre derivado del &lt;code&gt;run_id&lt;/code&gt;. No debe contener datos personales ni quedar reutilizable por otro job. El objeto que circula entre los tests puede ser tan pequeño como este:&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;MailboxLease&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;expires_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El &lt;code&gt;run_id&lt;/code&gt; es más importante de lo que parece. Si el pipeline reintenta una tarea, el segundo intento puede recibir un identificador nuevo y no confundir los mensajes del primero. Para borrar un lease, conviene usar una operación idempotente: si ya no existe, la limpieza sigue siendo correcta.&lt;/p&gt;

&lt;p&gt;También defino un estado explícito para la espera: &lt;code&gt;created&lt;/code&gt;, &lt;code&gt;message_found&lt;/code&gt;, &lt;code&gt;consumed&lt;/code&gt;, &lt;code&gt;expired&lt;/code&gt; o &lt;code&gt;failed&lt;/code&gt;. Los estados hacen que el log sea util, especialmente cuando el fallo ocurre en un runner remoto y no tenemos la bandeja abierta delante.&lt;/p&gt;

&lt;h2&gt;
  
  
  Un fixture pequeño con FastAPI y pytest
&lt;/h2&gt;

&lt;p&gt;El fixture puede crear el buzón antes de cada test y revocarlo al terminar. La API de correo queda detrás de un cliente, así el test no sabe si usa un servicio real, un servidor local o un double en desarrollo.&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;os&lt;/span&gt;
&lt;span class="kn"&gt;import&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;pytest&lt;/span&gt;


&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;mailbox&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mail_client&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;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CI_JOB_ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uuid4&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;
    &lt;span class="n"&gt;lease&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mail_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_lease&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="n"&gt;ttl_seconds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;300&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;lease&lt;/span&gt;
    &lt;span class="n"&gt;mail_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;revoke_lease&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lease&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_verificacion_de_usuario&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;mailbox&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mail_client&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="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;mailbox&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;solo-para-test&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="n"&gt;mail_client&lt;/span&gt;&lt;span class="p"&gt;.&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;mailbox&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;timeout_seconds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&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="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;En producción de tests, &lt;code&gt;wait_for_message&lt;/code&gt; debería filtrar por destinatario, asunto y un token de correlación. Leer “el último email” es una atajo peligroso: otro test puede terminar justo antes y dejar un resultado falso positivo.&lt;/p&gt;

&lt;p&gt;Si el equipo usa &lt;a href="https://dev.to/hannahdev56/saas-metricas-de-activacion-por-email-112j"&gt;medir la activación por email en un SaaS&lt;/a&gt;, el mismo &lt;code&gt;run_id&lt;/code&gt; puede conectar la entrega con el evento de activación sin guardar el contenido completo del mensaje. Menos datos retenidos significa menos ruido y menos riesgo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limpieza, expiración y diagnóstico
&lt;/h2&gt;

&lt;p&gt;La limpieza debe ocurrir en dos capas. Primero, el fixture revoca el buzón en el camino normal. Segundo, un proceso periódico elimina leases expirados. Así una cancelación del runner no deja recursos abiertos para siempre.&lt;/p&gt;

&lt;p&gt;En CI suelo guardar solo metadatos: &lt;code&gt;run_id&lt;/code&gt;, estado, latencia de entrega y causa de fallo. El cuerpo del correo se puede ocultar o borrar después de extraer el código. Para depurar, una traza como esta es suficiente:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;run=1842 state=created mailbox=ci-1842 expires_in=300s
run=1842 state=message_found wait=2.4s subject=Verifica tu cuenta
run=1842 state=consumed cleanup=ok
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Si necesitas un servicio externo para pruebas manuales, un &lt;a href="https://tempmailso.com" rel="noopener noreferrer"&gt;temp mail so&lt;/a&gt; puede ser útil para aislar registros exploratorios. Para una suite automatizada, verifica primero su política de retención, límites y estabilidad; no conviene convertir un servicio público en dependencia invisible del pipeline.&lt;/p&gt;

&lt;p&gt;Hay un detalle que suele colarse en tickets de QA: &lt;code&gt;fake e mail com&lt;/code&gt;. Lo trato como una búsqueda o etiqueta de diagnóstico, nunca como un criterio para aceptar un mensaje. El criterio real debe ser el &lt;code&gt;run_id&lt;/code&gt; y el destinatario exacto.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist para CI
&lt;/h2&gt;

&lt;p&gt;Antes de subir el flujo, reviso lo siguiente:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Cada ejecución obtiene un buzón o alias único.&lt;/li&gt;
&lt;li&gt;El lease tiene TTL y propietario (&lt;code&gt;run_id&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;La espera tiene timeout y no usa pausas fijas como estrategia principal.&lt;/li&gt;
&lt;li&gt;El mensaje se filtra por destinatario, asunto y correlación.&lt;/li&gt;
&lt;li&gt;La revocación se ejecuta incluso cuando el test falla.&lt;/li&gt;
&lt;li&gt;Existe una limpieza de respaldo para leases expirados.&lt;/li&gt;
&lt;li&gt;Los logs no imprimen códigos, tokens ni el cuerpo completo.&lt;/li&gt;
&lt;li&gt;Un reintento no consume mensajes de la ejecución anterior.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Este checklist parece pequeño, pero evita bastante trabajo repetido. Cuando el contrato está definido, cambiar de proveedor de email o ejecutar los tests en paralelo deja de ser una aventura.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  ¿Debo crear un buzón por test?
&lt;/h3&gt;

&lt;p&gt;No siempre. Un buzón por suite puede ser suficiente si cada mensaje lleva un identificador único y el consumo es estricto. Para pruebas paralelas o flujos sensibles, un lease por test reduce interferencias.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Qué timeout elegir?
&lt;/h3&gt;

&lt;p&gt;Empieza con el tiempo normal de entrega más un margen corto y mide. Un timeout de 30 segundos suele ser razonable para un entorno controlado, pero el valor correcto depende del proveedor y del runner.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Puedo guardar el email para investigar?
&lt;/h3&gt;

&lt;p&gt;Solo durante el tiempo imprescindible y con datos minimizados. Enmascara direcciones y elimina tokens después de la ejecución. Un buen diagnóstico no necesita convertir cada fallo en un archivo permanente.&lt;/p&gt;

&lt;p&gt;La idea central es simple: el correo de prueba también es infraestructura. Si tiene identidad, límites, estados y limpieza, FastAPI y CI pueden usarlo de forma predecible, incluso cuando los jobs corren al mismo tiempo.&lt;/p&gt;

</description>
      <category>python</category>
      <category>fastapi</category>
      <category>testing</category>
      <category>automation</category>
    </item>
    <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>
