Inbox
Back to Developers & QA

The difficult assertion is which code is current

How to Test Email OTP Flows Without Accepting the Wrong Code

Test email OTP flows around request boundaries, code recency, resend behavior, expiration, attempt limits, and the account state produced after the correct code is submitted.

8 min read

Email OTP tests become flaky when the mailbox contains several plausible codes and the test treats any recent-looking number as success. The real system has rules about which request created the code, how long it remains valid, whether resend invalidates older codes, and what happens after failed attempts. A good test makes those rules observable instead of hiding them behind a convenient code extractor.

Model OTP behavior as explicit state

EventState questionUseful assertion
First requestWhich code is now valid?Only the code tied to the current action is accepted according to policy
ResendDoes the old code remain valid?Test the product rule instead of assuming invalidation
ExpirationWhen does the code stop working?Expired code is rejected and the user gets a safe next step
Wrong attemptHow are repeated failures handled?Attempt counting, throttling, or lock behavior matches policy
Successful useCan the code be replayed?A one-time code should not silently behave like a reusable password if the design promises one-time use

A deterministic OTP test sequence

  1. 1

    Create a clean request boundary

    Use a fresh recipient or a mailbox where you can unambiguously distinguish the current run. Record the time and action that generated the OTP.

  2. 2

    Wait for the matching message, not merely the first numeric message

    Match sender, subject, recipient, and request timing before extracting the code.

  3. 3

    Submit the current code

    Assert both the immediate response and the resulting account or session state.

  4. 4

    Exercise resend separately

    Request a second code only when the test is specifically checking resend semantics, then test the first and second codes according to documented behavior.

  5. 5

    Test expiration and replay as their own cases

    Do not rely on a happy-path test to prove that old codes eventually fail or that a successful code cannot be used again.

Do not let retries mutate the security scenario invisibly

Playwright and Cypress both support retries, but a retried test can re-run setup and trigger a new OTP. If the test body requests another code on every attempt while the mailbox persists across attempts, a retry can change the very state the assertion is meant to diagnose.

Keep retry behavior visible. Either provision a fresh identity for each attempt or design fixtures so the test can tell which OTP request belongs to which retry. A flaky assertion should not be 'fixed' by silently accepting whichever code arrived last.

Email OTP is not phishing-resistant authentication

Email-delivered codes can be useful product mechanisms, but they should not be described as phishing-resistant simply because they are one-time. NIST's out-of-band guidance explicitly distinguishes email from device-bound out-of-band authenticators. For QA, test the product behavior you actually ship without overstating the security property of the channel.

OTP cases that deserve separate tests

  • Correct code on the first request.
  • Incorrect code and error handling.
  • Resend and old-code behavior.
  • Expiration boundary.
  • Replay after successful use.
  • Multiple rapid requests or throttling.
  • Code delivered to the wrong recipient or wrong environment.

Reliable OTP QA is mostly state control: know which request created the code, know which code should be valid now, and test resend, expiration, replay, and retry behavior as explicit product rules.

Need a clean recipient for manual email QA?

Use MailOnce to inspect real signup, OTP, magic-link, password-reset, and notification messages without putting a personal inbox into the test fixture.

Create temporary email

Sources & further reading

Primary security and test-runner documentation used to ground the testing guidance.

Continue testing

Move to the next boundary in the same email-testing problem space.