Matthew Boston

Use example.com in Your Tests

July 11, 2024

Every test suite is full of fake email addresses and URLs, and every one of them points somewhere. Make it example.com. RFC 2606 reserves it for documentation and testing, so a fixture address can’t land in a stranger’s inbox, and a sample URL in your README can’t send readers to whatever a stranger decides to host there.

The made-up domain somebody owns

Fake domains are rarely as fake as they look. test.com has been registered since 1997. acme.com has been registered since 1991. Whatever plausible name you invent for a fixture, there’s a decent chance someone already owns it, and they’ll receive whatever your code sends there.

Most of the time a fixture never leaves the test process, and nothing happens. The trouble starts when the data travels. A seed script loads sample users into a staging environment that has real SMTP credentials, and a background job sends each of them a welcome email. A webhook test runs from a laptop with a real HTTP client. A README tells readers to curl an endpoint on a domain that sounds made up and isn’t. Each time, someone outside your company gets your traffic, and possibly your data.

example.com closes that off. IANA operates it, and it publishes a null MX record (0 ., defined in RFC 7505) that tells mail servers the domain accepts no email. A welcome message to jane@example.com gets rejected on the spot instead of reaching a person.

What RFC 2606 reserves

RFC 2606 dates to 1999 and is short enough to read over coffee. It reserves four top-level domains and three second-level ones:

  • .test for testing.
  • .example for documentation and examples.
  • .invalid for names that are meant to be obviously invalid.
  • .localhost for the local machine.
  • example.com, example.net, and example.org for documentation and examples.

RFC 6761 later added these to a formal registry of special-use domain names, which spells out how resolvers and applications should treat each one. For everyday use, you only need to know the names are safe and nobody will ever register them.

Reserved doesn’t mean offline

example.com resolves. Run dig example.com and you’ll get real IP addresses back, and a browser will show you IANA’s placeholder page. That matters in tests. A test that makes an actual HTTP request to example.com still leaves your machine, still waits on DNS, and still flakes the way every network-bound test does. The reserved name keeps strangers out of the request. It doesn’t make the request free.

So pick the name to match what the test needs:

```ruby # Placeholder data that should never be contacted user = User.new(email: “jane@example.com”) webhook_url = “https://hooks.example.com/orders”

A host that must fail DNS lookup, for testing that error path

client = ApiClient.new(base_url: “https://api.unreachable.invalid”)

A local service you point at 127.0.0.1 in /etc/hosts

client = ApiClient.new(base_url: “http://api.myapp.test:3000”) ```

.invalid is the one people forget. When you want to test what your code does when a hostname doesn’t resolve, use a name the standard says never will. That’s more honest than a made-up domain you hope nobody registers.

Addresses and phone numbers have reserved ranges too

The same idea extends past domain names. RFC 5737 sets aside three IPv4 blocks for documentation: 192.0.2.0/24, 198.51.100.0/24, and 203.0.113.0/24. RFC 3849 reserves 2001:db8::/32 for IPv6. If your docs show a firewall rule, or a test needs a client IP, use one of those instead of an address that might belong to someone.

Phone numbers have a version of this as well. In the North American Numbering Plan, 555-0100 through 555-0199 are set aside for fictional use. If a fixture needs a phone number that will never ring, use one of those.

Make it the default

The easiest way to get this right is to make the safe value the one that’s already there. If you use FactoryBot, the factory is the place:

ruby factory :user do sequence(:email) { |n| "user#{n}@example.com" } end

Then catch the stragglers. This check prints any email address in spec/ that isn’t on a reserved domain, and exits non-zero if it finds one, so it can fail a CI build:

sh ! grep -rEno '[[:alnum:]._%+-]+@[[:alnum:].-]+\.[[:alpha:]]{2,}' spec/ \ | grep -vE '@([[:alnum:]-]+\.)*(example\.(com|net|org)|example|test|invalid)$'

It costs nothing to type example.com, and anyone reading your tests or docs knows at a glance it’s a placeholder. Nobody has to wonder whether billing@acme-corp.io is a real integration.

Use the names that were reserved for exactly this.