Developer Guides
Testing transactional email without sending any
Email is one of the few things in an application routinely shipped untested, because the obvious way to test it — send one and look — does not fit in a test suite.
Most of what can go wrong is testable without sending anything.
Unit: the content
Render the template with known data and assert on the output. No network.
- Every variable substituted — no stray placeholder tokens in the result
- Values that arrive from users are escaped; a name containing a tag does not become markup
- Links point at the right environment, not at localhost
- The plain-text alternative exists and contains the same essential information, including the link
The placeholder check catches the most common production embarrassment, and it is one assertion.
Integration: the call
Substitute a fake transport that records instead of sending. Then assert on what the application tried to do.
- Exactly one message was sent for one request — no duplicates
- It went to the right recipient
- The idempotency key is present and stable across retries
- A failure from the transport is handled rather than swallowed
- A suppressed recipient results in no send
That last one is worth an explicit test. Suppression logic that is never exercised is suppression logic that will not run.
Contract: the webhook handler
Your handler is an endpoint that mutates state based on external input. Test it like one.
- A valid signature is accepted
- An invalid signature is rejected
- An expired timestamp is rejected
- The same event ID delivered twice has the effect of one
- Events arriving out of order do not produce a wrong final state
See verifying webhook signatures for what each of these protects against.
Where a real send is required
Three things, and none of them belong in a unit test:
Rendering in real clients. Outlook, Gmail and Apple Mail disagree about CSS in ways no test catches. This needs rendering previews or real accounts.
Authentication as a receiver computes it. SPF, DKIM and DMARC alignment can only be confirmed by a receiver evaluating a real message. The inbox tester does exactly this.
Deliverability. Not testable in any meaningful sense before you send. It is monitored, not asserted.
Staging
The failure mode is a staging run that mails real customers. Two defences, both:
Route all non-production mail to a catch-all sandbox, so an address that escapes goes nowhere real. And enforce an allowlist in the application for non-production environments, so the code refuses to send to anything outside it.
One of these is a configuration someone can change. Two means a mistake has to happen twice.
DKIM signing, SPF alignment and DMARC are set up the moment you verify a domain.