OTP testing guide

How to test OTP flows without mocking email

Keep fast parser and service unit tests, then add a smaller end-to-end suite that proves the real message reaches a real inbox and completes the user flow.

By Evidence checked 9 min read Editorial method

Open the quickstart
Direct answer

Use a unique test mailbox, trigger the real OTP request, wait for an inbound message after the trigger time, validate sender and recipient context, extract the current code, submit it once, and assert the verified state. The test should fail at a bounded timeout and record redacted delivery evidence instead of sleeping or reading a shared inbox.

Visual guide

Decision map

Mocks and real email answer different questions

  1. 01
    Application and parser logic

    Mock provider calls, validate template variables, and run extraction fixtures across languages and false-positive cases.

  2. 02
    Provider and webhook behavior

    Verify signed callbacks, delivery state mapping, suppression, retries, and idempotent event storage.

  3. 03
    Actual user journey

    Send to an isolated inbox, retrieve the code, submit it, and assert the verified application state.

Test strategy

Mocks and real email answer different questions

A unit test can prove that application code calls an email client with the expected template data. It cannot prove DNS, provider acceptance, routing, parsing, actual template output, link generation, or final user verification.

Keep many fast unit tests and a smaller number of real-message tests. The end-to-end suite should target critical account journeys and produce diagnostics precise enough to identify the failing layer.

Unit

Application and parser logic

Mock provider calls, validate template variables, and run extraction fixtures across languages and false-positive cases.

Integration

Provider and webhook behavior

Verify signed callbacks, delivery state mapping, suppression, retries, and idempotent event storage.

End to end

Actual user journey

Send to an isolated inbox, retrieve the code, submit it, and assert the verified application state.

End-to-end flow

Build one deterministic OTP test

  1. 01

    Allocate a clean recipient

    Create an isolated mailbox and ensure no stale messages can match.

  2. 02

    Request the OTP

    Trigger the real application endpoint and record the recipient and request time.

  3. 03

    Wait for the current message

    Filter inbound email after the trigger with an explicit deadline and expected sender context.

  4. 04

    Extract and validate

    Parse the code, reject date-like or ambiguous candidates, and keep the message ID for diagnostics.

  5. 05

    Submit once

    Use the code and assert the account or session becomes verified.

  6. 06

    Test expiry and replay separately

    Request or retain a controlled code to prove old or reused values fail according to policy.

Bounded code wait
mails code --to "$TEST_EMAIL" --timeout 60
Edge cases

Exercise ambiguity, timing, and localization

Parser coverage and end-to-end coverage should share fixtures where possible, but the end-to-end test must still verify the actual application template and delivery path.

  • The message contains a year, date, order number, and the real code.
  • Two OTP requests produce two messages and only the newest code works.
  • The template uses Chinese, Japanese, or Korean code markers.
  • Delivery arrives after the test timeout and must not satisfy the next run.
  • The sender or link host is wrong even though the subject looks correct.
  • The code expires or is reused after successful verification.
Diagnostics

A failed OTP test should identify the layer

ObservationInterpretationNext check
No provider message IDApplication may not have sent.Queue, template validation, provider request logs.
Provider accepted, no inbox mailDelivery or routing issue.Domain DNS, provider state, inbound route.
Message arrived, parser emptyTemplate or extraction mismatch.Redacted content fixture and parser rules.
Code extracted, application rejectsToken state or timing issue.Expiry, newest-code policy, recipient binding, replay.
Verification succeeds, UI staleFrontend or session state issue.API response, cache, cookie, and navigation.
CI operation

Keep real-email tests small and observable

Run the critical paths on a cadence and environment that match their cost and stability. Do not make every unit test depend on external email, and do not hide provider outages by retrying indefinitely.

Track test volume, timeout rate, message latency, parser failures, and final verification separately. These metrics distinguish product bugs from provider or environment problems without collecting production mailbox content.

Implementation judgment

Keep the test faithful to the user journey

A passing assertion should prove that the current message completed the intended account state, not merely that a number appeared in an inbox. Bind every code to the recipient, request window, sender context, and final application outcome, then test expiry and replay separately.

Evidence

Sources and product scope

Product behavior is verified against the mails0 source and documentation. External comparisons link to official vendor documentation checked on 2026-08-15.

Questions and answers

Questions that come up in practice

Should all OTP tests use real email?

No. Keep many unit and integration tests with mocks or fixtures, then use a smaller end-to-end suite for the real delivery and verification journey.

How do I prevent stale codes from passing?

Use an isolated recipient, record the trigger time, match only messages after that boundary, and verify the application rejects older or reused codes.

Can mails0 extract non-English OTP templates?

The current parser includes common English, Chinese, Japanese, and Korean code patterns. Keep product-specific fixtures because template wording can still vary.

What should CI log on failure?

Log redacted message identifiers, timing, sender domain, recipient alias or hash, provider state, and parser outcome. Avoid full bodies, raw keys, and sensitive codes in broad artifacts.

Start with a bounded inbox

Give the agent an inbox, then prove the workflow.

Start with a receive-only temporary mailbox. Move to a permanent identity or self-hosted deployment only after the workflow reaches a real email value event.

Open the quickstart