otpbox

OTPBox / Guides / Selenium

Selenium

Test email verification with Selenium

A Selenium suite that stops at "enter the code we emailed you" either mocks the mail step or shares one hand-typed inbox across every test run — both are fragile. OTPBox gives a Python + Selenium test a real disposable address, a real inbox to poll, and a way to delete it when the test is done, so the OTP screen gets exercised the same way a real user hits it.

The pattern

Every WebDriver flow below is the same four steps: mint a key and create a real inbox, type its address into the signup form Selenium is driving, poll the inbox until the code shows up, then submit it and tear the inbox down. There's no first-party otpbox Python package documented, so the REST calls below use the plain requests library — it's a handful of lines and keeps the dependency list short.

Setup: a key and a pytest fixture

Mint a free key once (200 requests/month — see Get a key) and store it as OTPBOX_KEY in your environment. A small pytest fixture keeps the inbox lifecycle out of the test body:

import os
import time
import requests
import pytest
from selenium import webdriver
from selenium.webdriver.common.by import By

OTPBOX_KEY = os.environ["OTPBOX_KEY"]
BASE = "https://otpbox.io/api/v1"
HEADERS = {"authorization": f"Bearer {OTPBOX_KEY}", "content-type": "application/json"}


@pytest.fixture
def inbox():
    res = requests.post(f"{BASE}/inboxes", headers=HEADERS, json={})
    res.raise_for_status()
    box = res.json()  # {id, address, domain, expiresAt, token}
    try:
        yield box
    finally:
        requests.delete(f"{BASE}/inboxes/{box['id']}", headers=HEADERS)


@pytest.fixture
def driver():
    d = webdriver.Chrome()
    yield d
    d.quit()

The inbox fixture's finally block runs whether the test passes or fails, so a failed assertion never leaves the inbox orphaned — though the 15-minute expiry cron would clean it up either way.

The test: sign up, poll for the code, verify

Poll GET /api/v1/inboxes/:id/messages until a message has an extracted code. Selenium has no built-in async wait for an out-of-band event like this, so a small retry loop does the job (Selenium's own WebDriverWait is for DOM conditions, not for polling an external API):

def wait_for_otp(inbox_id, timeout_s=20, interval_s=1.5):
    deadline = time.monotonic() + timeout_s
    while time.monotonic() < deadline:
        res = requests.get(f"{BASE}/inboxes/{inbox_id}/messages", headers=HEADERS)
        res.raise_for_status()
        messages = res.json()["messages"]
        if messages and messages[0].get("code"):
            return messages[0]["code"]
        time.sleep(interval_s)
    return None


def test_signup_with_real_otp(driver, inbox):
    driver.get("https://your-app.example.com/signup")
    driver.find_element(By.NAME, "email").send_keys(inbox["address"])
    driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()

    code = wait_for_otp(inbox["id"])
    assert code is not None, "no OTP arrived before the timeout"

    driver.find_element(By.NAME, "otp").send_keys(code)
    driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()

    assert "Welcome" in driver.find_element(By.TAG_NAME, "body").text

Java + JUnit, if that's your stack

The shape is identical — only the HTTP client and assertion library change. Use java.net.http.HttpClient (built in since Java 11) for the REST calls and drive the same polling loop from an @AfterEach-style teardown:

HttpClient http = HttpClient.newHttpClient();
HttpRequest createInbox = HttpRequest.newBuilder()
    .uri(URI.create("https://otpbox.io/api/v1/inboxes"))
    .header("authorization", "Bearer " + otpboxKey)
    .header("content-type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString("{}"))
    .build();
HttpResponse<String> res = http.send(createInbox, HttpResponse.BodyHandlers.ofString());
// parse res.body() for id/address/token, then drive WebDriver and poll
// GET /api/v1/inboxes/{id}/messages the same way, reading .messages[0].code

A note on polling versus webhooks

The retry loop above is simple and fine for a local suite, but if your test runner already accepts inbound HTTP (for example a CI job with a reachable endpoint), you can register a webhook on otp.extracted instead of polling — see Webhooks. For most Selenium suites running against a browser anyway, a short poll loop like wait_for_otp above is simpler to reason about and doesn't require exposing anything.

CI notes

Next steps

Ready to try it against your own app? Create a free account

← Back to OTPBox