Email Testing Best Practices for Developers
Email is one of the flakiest things to test — it’s asynchronous, it lives outside your app, and the obvious approaches (a shared Gmail, a sleep() and a regex) fall apart fast. After building a lot of email tests, here are ten practices that keep them fast, reliable, and meaningful.
1. Use a disposable inbox, not a real mailbox
Real mailboxes need OAuth to read, collide across parallel runs, and force you to store credentials in CI. A disposable inboxis isolated, readable over an API, and needs nothing but an API key. It’s the foundation everything else builds on.
2. One inbox per test, not a shared one
Give every test (or every run) its own address with a unique prefix — a timestamp works. Shared inboxes mean tests read each other’s mail and fail intermittently under parallelism.
3. Wait, don’t sleep
sleep(5)is both slow and flaky — too short and the email hasn’t arrived, too long and your suite crawls. Block on a wait-for-email call that returns the instant the message lands.
4. Filter what you wait for
Don’t wait for “any” email — a welcome message can arrive before the code email and fail your assertion. Narrow it with from, subject, or has_code=true so you get exactly the message you mean.
5. Extract codes and links, don’t regex bodies
A \d{6} regex matches years, order numbers, and zip codes too. Read the parsed extractedCodes[0] and primaryLink fields instead — see testing OTP codes.
6. Assert on the email itself, not just the flow
Since you have the raw message, check it’s correct: the right sender and subject, and that it passes authentication.
expect(email.from).toContain('noreply@myapp.com')
expect(email.verdicts.dkim).toBe('PASS')That guards against deliverability regressions at the same time.
7. Clean up after each test
Delete the inbox when the test finishes (a fixture teardown is ideal) so leftover mailboxes don’t pile up against your plan’s limit and 403 your next run.
8. Keep the API key out of the repo
Read it from an environment variable (conventionally DEVMAILR_API_KEY) sourced from a CI secret — never commit it.
9. Test the negative cases
The happy path is the easy half. Also assert that a wrong or expired code is rejected, that codes/links are single-use, and that requesting a reset for an unknown email doesn’t leak whether the account exists.
10. Don’t re-test the provider on every run
You’re testing your integration— that your app sends the right email and your code handles it. You don’t need a full deliverability audit in every test. Keep one focused SPF/DKIM/DMARC check rather than asserting verdicts everywhere.
Put it into practice
These map cleanly onto specific stacks — see the guides for Playwright, Cypress, Python, and Node.js with Jest.
Try it in two minutes
Create a disposable prefix@devmailr.app inbox over the API, send mail to it from your app, and read it in your tests. Free plan, no card required.