How to Test Email Verification in Web Apps
Email verification is one of those features that's easy to implement but surprisingly tricky to test well. You need a real-looking inbox that actually receives emails, but using real addresses pollutes your user database and exposes developers to marketing. The testing strategy matters — here's how to approach it at each stage of development.
The testing environments and what each needs
| Environment | What you need | Best approach |
|---|---|---|
| Local development | Fast, no real emails sent | Fake SMTP server (Mailhog) |
| Staging | Real email delivery, controlled inboxes | Disposable email or Mailinator |
| CI/CD pipeline | Automated, programmatic inbox access | Mailtrap or Mailinator API |
| Manual QA | Quick, disposable, browser-based | InboxDrop or Guerrilla Mail |
| End-to-end tests | Deterministic, no flakiness | Intercepted SMTP or test email provider API |
Local development: Mailhog
For local development, never send real emails. Use Mailhog — a fake SMTP server that catches all outgoing emails and displays them in a local web UI (default: http://localhost:8025). No emails leave your machine.
Set it up in Docker:
docker run -d -p 1025:1025 -p 8025:8025 mailhog/mailhog
Then configure your app to use localhost:1025 as the SMTP host. All emails your app sends appear immediately in the Mailhog UI. You can inspect headers, HTML, and plain text rendering. The inbox resets on restart.
Alternative: Mailtrap (cloud-based) provides a similar intercepted inbox that works across team members without local setup. Useful when you need a shared test inbox.
Manual QA and staging: disposable email
When testing on a staging environment with real email delivery, disposable email services are the standard tool:
InboxDrop
Open InboxDrop, copy the generated address, use it in your staging registration. The confirmation email arrives in the browser-based inbox. No account needed, address expires after the session. Good for rapid iteration where you're running the same flow repeatedly.
Mailinator (public inboxes)
Any address @mailinator.com is publicly accessible at mailinator.com/v4/public/inboxes — no login, no setup. Any email sent to testuser123@mailinator.com is immediately visible. Important caveat: Mailinator inboxes are public and indexed — do not use them for testing if the verification email contains sensitive tokens or data that shouldn't be publicly accessible.
For private inboxes in automated tests, Mailinator's paid API provides programmatic access.
Automated testing: strategies and tools
Pattern 1: Intercept at the SMTP layer
The cleanest approach for end-to-end tests. Configure your test environment to use a fake SMTP server, then query that server's API to read emails programmatically:
// Using Mailtrap's API in a test const emails = await mailtrap.getEmails(inboxId); const verificationLink = extractVerificationLink(emails[0].html_body);
Pattern 2: Expose a test-only endpoint
For apps where you control the backend, add a test-only endpoint (behind a feature flag or environment check) that returns the verification token for a given email address:
// Only enabled in test/staging environments
GET /test/verification-token?email=test@example.com
→ { token: "abc123" }
This avoids email delivery entirely in automated tests and makes verification flows deterministic and fast.
Pattern 3: Short-circuit verification
Accept any code when a specific test email pattern is used (e.g. test+*@yourdomain.com). This requires no external tooling and makes tests completely reliable, though it means the verification flow isn't fully exercised end-to-end.
What to test in email verification
Beyond the happy path (user gets email, clicks link, account activated), test these scenarios:
- Expired tokens — verification links that have passed their expiry time should show a clear error and offer resend
- Already-used tokens — clicking a verification link a second time should not cause errors
- Email delivery failure — what happens when the email fails to send? Does the user see an error or get stuck?
- Resend flow — can users request a new verification email? Is there rate limiting?
- Case sensitivity — are email addresses normalised to lowercase before storage and lookup?
- Special characters — do email addresses with
+tags, dots, and international characters work?
Testing a registration flow manually? Get a fresh disposable email address for every test run.
Get a Free Temp Email