How to Test Email Verification Without Using a Real Email

By Denis · Published 2026-09-17.

Verification email is the gate in front of every signup flow, and the step QA most often leaves untested, because the usual tool, a personal inbox, is a terrible test fixture. Here is the setup that makes it an ordinary assertion.

Why a real inbox fails as a test fixture

A disposable inbox from Tempx fixes all four: any name at tempx.uk becomes a working mailbox the moment mail is sent to it, readable over a free JSON API, no account, no setup, no cleanup.

The recipe

  1. Pick a fresh address for the run. Derive it from something unique, verify-8f3a1@test.tempx.uk-style names never collide between parallel runs.
  2. Do the signup with that address in your browser, script or E2E test, exactly as a user would.
  3. Wait for the message, watch the inbox in the web app or a browser extension, or poll the inbox API: GET https://tempx.uk/api/inbox?address=… returns the message list as JSON, and MCP's wait_for_email blocks until it arrives (240 reads/minute is plenty for a 2-second poll).
  4. Assert on the message, sender, subject, and the code or link. Every message carries a 140-character text preview, which comfortably contains verification codes.
  5. Finish the flow, enter the code or open the link, and assert the account is activated.
# watch a verification mail arrive, then show it
ADDR="verify-8f3a1@tempx.uk"
for i in $(seq 1 30); do
 MSG=$(curl -s "https://tempx.uk/api/inbox?address=$ADDR" | jq -r ".messages[0].preview // empty")
 [ -n "$MSG" ] && echo "$MSG" && break
 sleep 2
done

What to assert (and what not to)

Assert onWhy
Sender addressCatches misconfigured From headers that tank deliverability
Subject lineThe template regression users see in notifications
The code / linkProves the flow is completable, beyond mail moving
Arrival timeYour verification SLA is part of the user experience
Exact HTMLDon't, templates change; assert structure or text, not pixels

Failure modes worth a test each

For frameworks, the same recipe has ready-made shapes: Playwright, Cypress and Selenium each ship a complete working spec.

Testing the negative space around the gate

The happy path proves the mail works; the edges prove the gate is a gate. Four checks, each one disposable inbox and a few lines: already-registered, signing up an existing address should say so (or send a "you have an account" mail), never silently create a duplicate; undeliverable domain, a syntactically valid address at a domain with no MX should fail gracefully, ideally after accepting (queue-and-bounce) rather than blocking signup, and either way the user must learn what to do next; resend cooldown, clicking "resend" six times yields one mail, or six, per your policy, but never six different valid codes when your policy says one; double-opt-in idempotency, clicking the confirmation link twice must not create two subscriptions or throw an error at the user.

All four share the assertion surface: the mailbox shows exactly what the policy promised, and the UI states match it. Because each check gets its own fresh inbox, the "exactly one mail" assertions stay deterministic even in a parallel run.

The deliverability half: proving it lands

Verification mail that renders beautifully and lands in spam is a broken flow with a passing test suite. Disposable inboxes run on real delivery, so they see the first half of deliverability (accepted or rejected at the border, and how fast) but not the per-provider placement (spam versus inbox) that only a real mailbox at each provider reveals. Split the concern: in the E2E suite, assert arrival and content; in a weekly scheduled check, send to a small set of real provider addresses and glance at where they landed.

For slow or odd arrival, the header analyzer on the received message answers most questions: the Received chain shows the delay hop-by-hop, and SPF/DKIM/DMARC verdicts explain most silent rejections before anyone opens a ticket with the mail vendor. The MIME decoder covers the content side, encodings, parts, and the text alternative your plain-text assertion depends on.

From test to standing guard

A verification spec run once proves nothing in October. The promotion path is boring and worth it: move the email specs into a scheduled lane (nightly or pre-release), give them generous timeouts, and let them be the canary for DNS changes, vendor outages and template regressions, the three things that break verification mail without touching any code. A one-address canary plus a two-address E2E run catches nearly all of it for pennies of compute.

Wire the failure output for the human who follows: the address used, the subject matched, the wall-clock waited. Half of canary debugging is reproducing the exact state; an address that still holds its mail turns that from archaeology into a browser tab.

When the sender is a third-party provider

Many apps do not send their own verification mail (an email API or auth provider does) and the test surface shifts accordingly. Your assertions move to contract-level: the provider accepted the send request, the mail arrived at your disposable inbox with the expected metadata (sender, subject, the merge fields your template promised), and the link or code it carries completes your flow. The provider's internals stay out of scope, their deliverability is their canary to run, and your suite should assert on arrival, not on their dashboard.

Two provider-flavored gotchas worth their own assertion: sandbox modes that accept sends but deliver nowhere (your disposable inbox will rightly see nothing, assert at the API layer instead), and webhooks (delivery events to your backend) that fire independently of the mail itself, test the webhook path with the provider's own test events, and the mail path with the real inbox, and keep the two assertions from masquerading as each other.

Documenting the contract you are enforcing

An email-verification suite quietly encodes product decisions (resend cooldown seconds, code TTL, duplicate-signup wording) and those decisions deserve daylight. The habit that pays: each assertion carries a comment naming the product rule it enforces ("cooldown: 60s, per product doc #142"), so the suite reads as a checklist of promises rather than a pile of green ticks. If product changes a rule, the comment points the weary engineer to the exact spec to update, and the suite's history becomes the contract's history.

The alternative (undocumented assertions) fails in a specific, expensive way: someone "fixes" a flaky-looking cooldown spec by loosening it, and the loosened rule ships silently. A suite that explains itself is the difference between guardrails and tripwires, and it costs one sentence per assertion.

Create a temporary email address

Free, instant, no sign-up, messages auto-delete.

Go to your inbox