DEV Community

Pudiu
Pudiu

Posted on AI-assisted

How to Test Email Verification Flows Without Polluting Your Inbox


Every registration flow has an email step, and testing it properly is more annoying than it should be.

You need a fresh, receivable address that has never been seen by the system under test. You need it to work from CI. You need enough of them to test both the success path and the failure paths. And if you reuse addresses, you end up with verification links from previous runs mixed into the current one, which is a confusing way to spend an afternoon.

Here's what I've settled on after doing this for a while.

The three failure paths you're probably not testing

Most people test the happy path: submit a valid email, receive the link, click it, account activates. That's one of four behaviours your code needs to handle.

1. Disposable / blocklisted domain. Most serious products reject known disposable email domains at registration. If you've never tested this path, you don't actually know whether your rejection logic fires before or after your user record is created. I've seen this create orphaned rows more than once.

2. Non-existent domain. test@thissitedoesnotexist-9f3a.com should be rejected. It usually is — but the error message is often the generic "invalid email," which is a bad experience for someone who simply typo'd.

3. Valid format, undeliverable mailbox. A syntactically perfect address on a real domain that has no mailbox. Here your validation passes, the email dispatches, and you find out via a bounce three minutes later — possibly after you've already created the account and started a trial.

4. Rate limiting. Twenty signups from the same IP in a minute. Do you have a limiter? Does it fire before or after account creation?

Why disposable inboxes make this easier

For paths 1 and 2 you can just hardcode test values. For the happy path and for "does the mail actually arrive and does the link actually work," you need a real receivable inbox.

Disposable email services give you that. The workflow:

# In your test setup, get a fresh address
ADDR="test-$(date +%s)@example-disposable-domain.com"
Enter fullscreen mode Exit fullscreen mode

Then assert that your app sent to it, and drive the verification via the provider's inbox.

Three things to check before you pick one:

Domain rotation. This is the one people miss. If your application is configured to reject disposable domains — which it probably should be — then your test inbox's domain needs to be one your blocklist doesn't cover. Otherwise you're trying to test the blocklist with an address the blocklist blocks. Being able to switch the receiving domain lets you test both sides deliberately.

Inbox lifetime. Your CI run needs to finish before the address expires. Ten-minute inboxes are a bad fit for a suite with a slow integration step. Something in the hour-plus range is comfortable.

Private vs shared inboxes. Some services, Mailinator most famously, use public inboxes — any request for the same address returns the same mail. That's fine for a throwaway sandbox assertion. It is not fine if your test fixture contains real names, real tokens, or anything resembling production data. Check which model you're using before you put real-looking data through it.

A note on the assertion step

The mistake I made early on was asserting on email content. Templates change, and a suite that breaks every time someone edits a word in a template gets disabled within a month.

Assert on the things that actually matter:

  • The outbound message was dispatched (sender address correct, recipient correct)
  • Exactly one message arrived, not zero and not two
  • The verification link follows the expected URL shape and resolves to a 2xx
  • The token in the link is accepted exactly once, and rejected on the second attempt

That last one is where real bugs live. Single-use token enforcement is easy to get wrong and rarely tested.

Cleanup

Whatever provider you use, assume the inbox disappears. Don't build a test suite that depends on an address still existing tomorrow — it will work locally and fail in CI on a schedule, which is the worst possible failure mode to debug.

For my own projects I've been using Pudiu Temp Mail because it covers the three criteria above — rotatable domains, a 24-hour inbox rather than ten minutes, no account required to start — and it supports ten locales, which is useful when you're testing localized templates. But the three selection criteria matter more than the specific provider; any service that satisfies them will work.

The paths most teams skip are the blocklist rejection and the single-use token check. Those are also where the bugs are.

Top comments (0)