Testing signup emails in CI without a real mailbox
31 July 2026 · 5 min read
The usual test does this: trigger a signup, sleep ten seconds, poll a mailbox, hope. It fails on a slow day and wastes ten seconds on every fast one.
Use a catch-all, not an inbox per test
A sandbox with plus-addressing means every actor in a test has their own address without creating anything first. `yours+alice@`, `yours+bob@`, `yours+run-8412@` all land in one place, and each message records which address received it.
const sb = await tm.createSandbox("checkout-tests");
await signUp({ email: sb.catchAll.replace("anything", "alice") });
const msg = await tm.waitForMessage(sb.id, { to: "alice" });
expect(msg.otp).toBeTruthy();Wait for a condition, not a duration
A helper that polls until a matching message arrives is faster than any fixed sleep and more reliable than a short one. When it gives up it should say what it was looking for and how many other messages did arrive, because "timed out" alone will not tell you the mail went to the wrong address.
Let the code be extracted for you
One-time codes and sign-in links are the thing you actually want. Have the service pull them out, so your test is not maintaining a regular expression against somebody else’s template.
More on for developers
Webhook signatures, done properly
A shared secret in a header is not a signature. Here is what to send, what to check, and the two mistakes almost everyone makes on the receiving end.
Rate limiting on the wrong axis is worse than none
Per-IP limits punish shared offices and mobile networks while doing nothing to an attacker who rotates addresses for pennies.
Designing a signup form that does not leak who is registered
"That email is already taken" is a feature request and a privacy hole, and you usually have to pick.