惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

人人都是产品经理
人人都是产品经理
Stack Overflow Blog
Stack Overflow Blog
S
SegmentFault 最新的问题
博客园 - 司徒正美
aimingoo的专栏
aimingoo的专栏
U
Unit 42
GbyAI
GbyAI
B
Blog RSS Feed
博客园 - Franky
L
LangChain Blog
Hugging Face - Blog
Hugging Face - Blog
美团技术团队
The GitHub Blog
The GitHub Blog
Y
Y Combinator Blog
云风的 BLOG
云风的 BLOG
H
Hackread – Cybersecurity News, Data Breaches, AI and More
博客园 - 三生石上(FineUI控件)
Microsoft Azure Blog
Microsoft Azure Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
G
Google Developers Blog
Last Week in AI
Last Week in AI
阮一峰的网络日志
阮一峰的网络日志
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Apple Machine Learning Research
Apple Machine Learning Research

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
How to Test Email Verification Flows in Python with pytest
zerodrop · 2026-06-27 · via DEV Community
Cover image for How to Test Email Verification Flows in Python with pytest

zerodrop

Testing email verification in Python is one of those problems that looks simple until you actually try it.

Your app sends a verification email. Your pytest test needs to read that email, extract the OTP or magic link, and continue the test. But the email lands in an inbox your test can't reach.

The common workarounds:

  • Mock the email layer — your tests pass while production email could be broken
  • Use a shared Gmail inbox — race conditions everywhere in parallel runs
  • Use MailHog — requires Docker, doesn't test your real email provider
  • Write regex against the email body — breaks every time the template changes

There's a cleaner way.

The ZeroDrop Approach

ZeroDrop gives you real disposable email inboxes caught at Cloudflare's edge. No Docker. No shared inbox. No regex.

When an email arrives, OTP codes and magic links are auto-extracted before the email is stored. By the time your test calls wait_for_latest(), email.otp and email.magic_link are already there.

email = mail.wait_for_latest(inbox)
email.otp        # "847291" — auto-extracted, no regex
email.magic_link # "https://..." — auto-extracted, no HTML parsing

Setup

pip install zerodrop

No dependencies. No API key. No signup. Python 3.8+.

Basic pytest Example

import pytest
from zerodrop import ZeroDrop

mail = ZeroDrop()

def test_signup_email_verification(page):
    # Generate a unique inbox for this test — no network request
    inbox = mail.generate_inbox()

    # Sign up with the ZeroDrop inbox
    page.goto("/signup")
    page.fill('[name="email"]', inbox)
    page.fill('[name="password"]', "TestPassword123!")
    page.click('[type="submit"]')

    # Wait for the verification email
    email = mail.wait_for_latest(inbox, timeout=15000)

    # OTP is auto-extracted — no regex needed
    assert email.otp is not None
    page.fill('[name="otp"]', email.otp)
    page.click('[type="submit"]')

    assert page.url.endswith("/dashboard")

No helper functions. No regex. No HTML parsing.

pytest Fixtures

For cleaner tests, wrap ZeroDrop in a pytest fixture:

# conftest.py
import pytest
from zerodrop import ZeroDrop

@pytest.fixture(scope="session")
def mail():
    return ZeroDrop()

@pytest.fixture
def inbox(mail):
    """Fresh inbox per test — no shared state."""
    return mail.generate_inbox()

Then use them in tests:

# test_auth.py
def test_signup_verification(page, mail, inbox):
    page.goto("/signup")
    page.fill('[name="email"]', inbox)
    page.fill('[name="password"]', "TestPassword123!")
    page.click('[type="submit"]')

    email = mail.wait_for_latest(inbox, timeout=15000)

    assert email.otp is not None
    page.fill('[name="otp"]', email.otp)
    page.click('[type="submit"]')

    assert page.url.endswith("/dashboard")


def test_magic_link_login(page, mail, inbox):
    page.goto("/login")
    page.fill('[name="email"]', inbox)
    page.click('button:text("Send magic link")')

    email = mail.wait_for_latest(inbox, timeout=15000)

    assert email.magic_link is not None
    page.goto(email.magic_link)

    assert page.url.endswith("/dashboard")


def test_password_reset(page, mail, inbox):
    page.goto("/forgot-password")
    page.fill('[name="email"]', inbox)
    page.click('[type="submit"]')

    email = mail.wait_for_latest(inbox, timeout=15000)

    assert email.magic_link is not None
    page.goto(email.magic_link)

    page.fill('[name="password"]', "NewPassword123!")
    page.click('[type="submit"]')

    assert page.locator(".success-message").is_visible()

Email Filtering

When your signup flow sends multiple emails — welcome email, verification email — filter to target the right one:

from zerodrop import ZeroDrop, ZeroDropFilter

mail = ZeroDrop()

def test_signup_otp_only(page, inbox):
    page.goto("/signup")
    page.fill('[name="email"]', inbox)
    page.click('[type="submit"]')

    # Only catch the verification email
    email = mail.wait_for_latest(
        inbox,
        timeout=15000,
        filter_=ZeroDropFilter(
            from_="noreply@yourapp.com",
            has_otp=True,
        )
    )

    assert email.otp is not None
    page.fill('[name="otp"]', email.otp)
    page.click('[type="submit"]')

All string filters are case-insensitive partial matches.

Parallel Test Runs — No Collisions

generate_inbox() runs locally with no network request. Each worker gets a unique inbox automatically:

# pytest.ini or pyproject.toml
# [tool.pytest.ini_options]
# addopts = "-n auto"  # pytest-xdist parallel execution

# Each test gets its own inbox — generate_inbox() is local, instant
def test_user_a(page, mail):
    inbox = mail.generate_inbox()  # unique per test
    # ...

def test_user_b(page, mail):
    inbox = mail.generate_inbox()  # different inbox, zero collision
    # ...

50 parallel workers. 50 isolated inboxes. Zero race conditions.

GitHub Actions CI

name: E2E Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: pip install -r requirements.txt
      - run: playwright install --with-deps chromium
      - run: pytest tests/e2e/ -v

No Docker. No SMTP service. No API keys in CI secrets. ZeroDrop works out of the box.

Why Not Use MailHog?

MailHog is a local SMTP server that catches emails in development. It works fine locally — but in CI it requires:

  • A Docker service block in your GitHub Actions YAML
  • Running against a local app instance, not your real staging environment
  • Manual OTP extraction via regex on the raw email body

ZeroDrop works against your real staging environment, in real CI, with no infrastructure overhead. email.otp is just there.

MailHog ZeroDrop
Docker required
Tests real email provider
OTP auto-extraction
Magic link extraction
Parallel-safe
CI setup Complex None

ZeroDropEmail Fields

email.otp        # "123456" — 4-8 digit code, or None
email.magic_link # "https://app.com/verify?token=abc" — or None
email.subject    # "Verify your email"
email.body       # Full plain-text body
email.from_      # Sender address
email.received_at # datetime

Both otp and magic_link are None if not detected. Always assert before using:

assert email.otp is not None
page.fill('[name="otp"]', email.otp)

Conclusion

Testing email verification in Python doesn't require Docker, regex, or a shared inbox. ZeroDrop gives each pytest test a real isolated inbox, with OTP and magic link auto-extracted at the edge.

email.otp is just there. No regex. No HTML parsing. No maintenance.

Free to use. No signup required. Works in CI out of the box.

zerodrop.dev · PyPI · GitHub