Test addresses that do not cause incidents

Testing anything that sends mail runs into the same question: what address do you put in? The wrong answer produces a class of bug that is embarrassing rather than merely broken — mail from your staging environment arriving at a real person.
First decide whether the mail has to arrive
This single question decides everything, and it is worth asking explicitly because the answer is “no” more often than people assume.
| What you are testing | Does mail need to arrive? | Use |
|---|---|---|
| Validation, storage, display of an address | no | a reserved domain — see below |
| The confirmation flow, by hand | yes | a mailbox here |
| The confirmation flow, automated | yes, readable by code | a service with an API |
| Load or volume testing | no — and must not | a reserved domain, always |
When mail must not arrive: use reserved names
Two domains exist precisely for this and are guaranteed never to belong to anyone: example.com and its siblings example.net and example.org, reserved by RFC 2606, plus the whole .test top-level domain.
Addresses at these are the correct answer for fixtures, seed data and load tests. Nothing you send can reach a person, because there is nobody there — by international agreement rather than by luck.
test@mycompanytest.com is somebody's real domain often enough to matter, and a load test aimed at it is indistinguishable from an attack. The reserved names cost nothing and remove the entire category of accident.When mail must arrive, by hand
Take an address here, run the flow, read what your application actually sent — headers, links, and how the message renders when it is not your own mail client displaying it. Two things this catches that a local mail catcher does not:
- Real delivery. The message crosses a real SMTP path, so misconfigured senders fail here and not in production.
- How it looks to a stranger. Links that only work in your session, images that never load, an unsubscribe footer pointing at localhost — all visible immediately.
Watch the attachment warnings too, if your application sends files: they are the same warnings your users will see.
When it must be automated
Scraping the page instead is a bad trade even when it works: it is brittle, undocumented, and it is exactly the traffic pattern that eventually gets blocked.
Testing rejection paths
The interesting failures are not “a valid address works”. Worth covering:
- An address at a domain with no MX record — delivery fails without the address being malformed. Your code should distinguish those. Check which is which.
- An internationalised domain, in both the Unicode and
xn--forms. Most validators reject one and accept the other, and it is usually the wrong way round — converter. - Gmail dot and plus variants. If your uniqueness check compares raw strings, one person can register unlimited accounts. Normalise first — that page does exactly what your check should.
- A very long local part. The standard allows 64 characters; plenty of forms truncate silently at 20 and store something that no longer resolves.
One thing to build, not to test
If your product refuses disposable addresses, refuse them clearly. “Invalid email” for a perfectly valid address is the single most confusing error in the category: the user checks the spelling, retypes it, and gets the same message. “We do not accept temporary addresses” takes the same space and ends the confusion.