Email testing with Selenium and Python
A pytest fixture that gives each test a fresh inbox, and one call that returns the verification code. It works with Selenium, Playwright for Python, or plain requests.
1. Install
pip install pytest requests selenium
export MAILFORNET_API_KEY="mf_live_your_key"
2. The fixture
In conftest.py:
import os, time
import pytest, requests
API = "https://api.mailfornet.com/v1"
class Inbox:
def __init__(self, session, address):
self.session, self.address, self.since = session, address, int(time.time() * 1000)
def wait_for_code(self, subject=None, sender=None, timeout=60):
"""Blocks until a verification code or confirm link arrives. Returns the JSON body."""
params = {"wait": min(timeout, 60), "since": self.since}
if subject:
params["subject"] = subject
if sender:
params["from"] = sender
r = self.session.get(f"{API}/inboxes/{self.address}/code", params=params, timeout=timeout + 10)
if r.status_code == 404:
raise AssertionError(f"No verification email reached {self.address} in {timeout}s")
r.raise_for_status()
return r.json() # {"code": "...", "link": "...", "subject": "...", ...}
@pytest.fixture(scope="session")
def mailfornet():
s = requests.Session()
s.headers["Authorization"] = f"Bearer {os.environ['MAILFORNET_API_KEY']}"
return s
@pytest.fixture
def inbox(mailfornet):
r = mailfornet.post(f"{API}/inboxes", json={"ttlMinutes": 30})
r.raise_for_status()
box = Inbox(mailfornet, r.json()["address"])
yield box
mailfornet.delete(f"{API}/inboxes/{box.address}")
3. A Selenium test
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import pytest
@pytest.fixture
def driver():
d = webdriver.Chrome()
yield d
d.quit()
def test_signup_verification(driver, inbox):
driver.get("https://staging.yourapp.com/signup")
driver.find_element(By.NAME, "email").send_keys(inbox.address)
driver.find_element(By.NAME, "password").send_keys("Correct-Horse-9")
driver.find_element(By.CSS_SELECTOR, "button[type=submit]").click()
mail = inbox.wait_for_code(subject="Verify")
assert mail["code"] and len(mail["code"]) == 6
driver.find_element(By.NAME, "code").send_keys(mail["code"])
driver.find_element(By.CSS_SELECTOR, "button[type=submit]").click()
WebDriverWait(driver, 10).until(EC.text_to_be_present_in_element((By.TAG_NAME, "h1"), "Welcome"))
def test_confirm_link(driver, inbox):
driver.get("https://staging.yourapp.com/signup")
driver.find_element(By.NAME, "email").send_keys(inbox.address)
driver.find_element(By.CSS_SELECTOR, "button[type=submit]").click()
link = inbox.wait_for_code(subject="Confirm")["link"]
driver.get(link)
assert "confirmed" in driver.page_source.lower()
4. API-only tests (no browser)
Testing a backend that sends mail? The same fixture works without Selenium:
def test_signup_api_sends_code(inbox):
requests.post("https://staging.yourapp.com/api/signup", json={"email": inbox.address}).raise_for_status()
mail = inbox.wait_for_code()
assert mail["from"].endswith("@yourapp.com")
assert mail["code"]
Parallel runs
With pytest-xdist (pytest -n 8) each worker creates its own inboxes, so tests never
read each other's mail. Nothing needs to change in the fixture.
Reading the full email
full = mailfornet.get(f"{API}/messages/{mail['messageId']}", params={"address": inbox.address}).json()
assert "Welcome to YourApp" in full["text"]
print(full["links"]) # every link, with its anchor text
print(full["verificationLink"]) # the one that confirms something
Related
Playwright guide · Cypress guide · Code examples · API reference