Skip to main content
Email testing proves that your app sent the right email to the right address, that it arrived, and that the code or link inside it works. A test triggers the send, waits for the message in a mailbox it controls, and asserts on what it reads.

How it works

A testing mailbox receives mail at addresses your test controls and lets the test read it through an API:
  1. Get an address that belongs to this test and no other.
  2. Trigger the email: submit a sign-up form, request a password reset.
  3. Wait for the message to arrive at that address.
  4. Read and assert: the subject, the recipient, the one-time code, the link.

Three kinds of mailbox

A DevHelm testing mailbox and the email alert channel are separate features: one receives mail for tests, the other sends notifications to people. Messages in a public disposable-inbox service are readable by anyone with the address, which is the wrong place for one-time codes and password-reset links, even in staging. A testing mailbox accepts mail from anyone, like any address, but reading it needs access to your workspace.

Assigned domain or your own

Every address needs a domain whose MX record points at the testing mailbox. Start with the assigned domain. Move to your own when your app only accepts sign-ups from certain domains, or when addresses have to look like yours. Receive on your own domain covers the DNS. On either domain, every local part is its own inbox. There is nothing to create per address, so a test can use a fresh address on every run.

Inject or real delivery

Inject runs the same parsing as real mail, including OTP and link extraction, so a test written against inject keeps working when you switch to real delivery. Use inject for fast tests and tutorials. Use real delivery for the end-to-end test that proves users actually get the email.

Why “the provider returned 250” is not proof

When your app hands a message to a mail provider, the provider answers right away: an SMTP 250 or an HTTP 200 or 202. That answer means accepted for delivery. It does not mean delivered. Between the two, the message can be:
  • held by a sandbox or test mode that accepts mail and never sends it,
  • dropped because the address is on the provider’s suppression list after an earlier bounce,
  • queued behind rate limits, or retried after a temporary failure at the receiving server,
  • sent to the wrong address by a template or configuration bug.
DevHelm’s inject works the same way: it answers 202 when it accepts the message, before the message is stored. A message accepted right at the organization’s monthly quota can still be dropped. The proof is the message itself, readable in the mailbox. That is why a test waits for the message instead of checking the send call’s status.

Waiting for the message

A wait call blocks until a matching message is stored, then returns it, or fails with 408 WAIT_TIMEOUT. It matches messages received at or after a start time, so take that time before the trigger:
The DevHelm CLI and SDKs default the start time to the moment you call wait, which misses a message that arrived first. Pass it explicitly. A fresh address per test means an older message cannot match. Reusing one address across runs is how a test ends up reading yesterday’s code.

Reading what is inside

Each message comes with the codes and links already extracted. A code labelled in the email, as in “Your verification code is 482913”, appears in otp, and each link appears in links with its text. Click tracking from your mail provider rewrites links to the provider’s redirect host, and the testing mailbox keeps them as written. Read OTP codes and links covers several codes and tracked links.

Keep it to test data

A testing address receives any mail sent to it. Use it for test accounts only, never for real users, real accounts, or customer data. Security and data lists who can read what.

DevHelm email testing

Your first test email

Get an address, inject a message, and assert on the code.

How testing mailboxes work

Domains, stored fields, and every way to read a message.