Article featured image
Email OTP testing: Complete guide for developers and QA teams
14min readLast updated: May 19, 2026

1. What are One-Time Passwords (OTPs)?

A One-Time Password (OTP) is a temporary, automatically generated code used to verify a user's identity. Unlike static passwords, OTPs expire after a short window (typically 30 seconds to 10 minutes) and can only be used once, making them significantly harder to intercept or reuse.

How OTPs are generated

OTPs differ in how the code is produced. There are three main types:

Type How it works
TOTP (Time-based) Shared secret + current timestamp
HOTP (Counter-based) Shared secret + incrementing counter
Random OTP Cryptographically random, server-generated

TOTP (Time-Based One-Time Password) codes are generated using a shared secret key and the current timestamp. Both the server and client independently generate the same code at the same time, so the OTP itself does not need to be sent by the server. Codes typically refresh every 30 seconds and expire after that window.

HOTP (HMAC-Based One-Time Password) works similarly, but uses an incrementing counter instead of time. The code does not expire based on time, but remains valid until it is used or falls outside the server’s allowed counter window. This makes it suitable for hardware tokens and offline scenarios where clock synchronization isn’t reliable.

Random OTPs are generated server-side using a cryptographically secure random number generator. The server stores the code (ideally hashed) and sends it to the user (usually via email or SMS). These codes expire after a short time and are invalidated once used.

How OTPs are delivered

The way an OTP is generated is independent of how it is delivered. In practice, most systems rely on one of these channels:

Email: The server generates a one-time password and sends it to the user’s inbox. This typically uses a random, server-generated OTP, since there is no shared secret or authenticator app involved. Email is one of the most common channels used in authentication flows.

SMS: The OTP is sent as a text message to the user’s phone number. Like email, this usually relies on a random OTP generated server-side, with the phone number acting as the delivery channel.

Authenticator apps: Apps like Google Authenticator generate OTPs locally on the device using TOTP (Time-based One-Time Password). The server and app independently compute the same code using a shared secret and the current time. The server does not send the OTP — the user enters the code generated on their device during login.

2. What is OTP testing?

OTP testing is the process of verifying that your one-time password flow works correctly end-to-end, from code generation and email delivery, to user submission and validation.

It’s worth distinguishing this from simply unit testing your token generation logic. Generating a valid code server-side is only one part of the flow. OTP testing covers the full path a user actually takes, especially in email OTP workflows:

  • The code is generated correctly and stored securely
  • The email is delivered to the right inbox on time (and not marked as spam)
  • The code can be extracted from the email reliably
  • It validates successfully within the expiry window
  • It fails correctly when expired, already used, or malformed

If any step in that chain breaks, users get locked out. That usually leads to support tickets — and quickly erodes trust in your authentication flow.

3. Why is OTP testing important for authentication flows?

Example of an OTP email used to verify a user during sign-in

OTP failures are hard blockers

OTP flows may look simple, but they depend on multiple moving parts — code generation, email delivery, expiry, and validation. If any one of these fails, users can’t log in, sign up, or recover their account. Unlike minor UI issues, this is a hard blocker that quickly leads to drop-offs and support tickets.

It directly impacts conversions and reliability

OTP failures affect critical user journeys. Delayed or missing codes reduce signup and checkout completion rates, especially in email OTP flows where delivery timing and spam filtering play a role. These issues often don’t appear in low-traffic environments but show up during spikes, when delays can cause codes to expire before they’re used.

Issues are easy to miss without testing

Authentication logic changes often, and without end-to-end OTP testing, regressions can slip into production. Because these flows depend on multiple systems, problems aren’t always obvious until users start failing to log in — which also impacts long-term trust.

4. Best ways to test email OTPs

There are a few different approaches to testing email OTP flows, each suited to different stages of development and levels of coverage. Most teams end up using a combination of these depending on what they're testing and how far along they are.

Inspecting emails in testmail.app's visual email viewer

Manual testing

Manual testing is the most straightforward approach. A tester triggers the OTP flow by hand, checks a real inbox, enters the code, and validates the outcome. It requires no setup and is useful during early development when you're still building the flow and want quick feedback.

The downside is that it doesn't scale. Manually checking inboxes for every test run is slow and error-prone, and it's not something you can integrate into a pipeline. It's also difficult to test edge cases like expired codes or invalid submissions consistently.

API-only testing

API-only testing skips the browser entirely and validates the OTP logic directly through your backend. You call your API to trigger code generation, then call the validation endpoint with the code to verify it works. This is faster than full end-to-end testing and useful for checking the core logic in isolation: does the token generate correctly, does it expire as expected, does it reject reuse?

The limitation is that it doesn't cover the full user journey. You're not testing whether the email actually arrives, whether the code is correctly embedded in the email body, or whether the frontend handles the submission correctly. It's a good complement to E2E testing, not a replacement.

End-to-end (E2E) automated testing

End-to-end testing covers the full flow: triggering the OTP, receiving the email in a test inbox, and submitting the code. This is the most comprehensive way to test email OTP flows and the approach most teams should aim for.

The main challenge has traditionally been the email step. Automating a real webmail UI (like Gmail) is slow and brittle, and shared inboxes can introduce state issues between test runs. Using programmable inboxes — such as those provided by testmail.app — solves this by letting you create isolated inboxes on demand and access them via API from any testing framework. Each test run gets a fresh inbox, which removes flaky state and makes message matching deterministic.

This setup makes it possible to reliably run automated OTP tests in CI/CD pipelines, where tests execute on every deploy. Without proper isolation, shared inboxes can cause flaky tests when emails from previous runs interfere with matching. Timing is another common issue — polling for emails at fixed intervals can fail depending on delivery speed. Using long-polling approaches, such as keeping the connection open until the email arrives (as supported by testmail.app's live query functionality), helps avoid arbitrary waits and makes tests faster and more reliable in CI.

💡
Test email OTPs with testmail.app
testmail.app gives you programmable inboxes you can generate on demand and access via API, making it useful across all of the approaches above. Get started →

5. What to test in an OTP email workflow

A common mistake is treating OTP testing as simply verifying that a code works when submitted correctly. In practice, a complete OTP testing strategy needs to cover multiple layers of the flow — especially in email OTP testing, where delivery and timing introduce additional complexity.

Email delivery and timing

The first thing to verify is that the OTP email actually arrives. Delivery is one of the most common failure points, particularly in staging and CI/CD environments where infrastructure may differ from production.

Key checks include:

  • Whether the email arrives within an acceptable time window
  • Whether it lands in the inbox instead of spam
  • Whether it’s sent from the correct sender and domain

It’s also important to test delivery under load. An OTP that arrives instantly during a single test run may behave very differently when many users trigger the flow at once. If your provider has sending limits or rate caps, those should be part of your OTP test cases.

Testing across providers (e.g. Gmail, Outlook, corporate inboxes) is also important, since spam filtering and delivery rules can vary significantly.

Code format and extraction

Once the email arrives, validate the OTP itself. This includes checking format (numeric or alphanumeric, expected length), consistency, and whether it can be reliably extracted from the email body.

This is a key part of automated OTP testing, where tests depend on extracting the code programmatically. If your format changes (for example, from a 6-digit code to an alphanumeric token), extraction logic can silently break.

It’s also worth testing extraction against both HTML and plain text versions of the email, since different clients may render either version.

Expiry

OTP expiry is one of the more important security properties to test, and also one that's easy to skip because it requires waiting. A few things to cover:

  • Does the code reject correctly after the expiry window?
  • Does the expiry window match what's communicated to the user in the email?
  • Does requesting a new code invalidate the previous one?

For automated tests, seeding the test with a known timestamp or manipulating the system clock tends to be a cleaner approach than actually waiting out the expiry window. It makes tests faster and keeps them deterministic, which matters in CI where a test that waits 10 minutes for an expiry check will quickly become a bottleneck.

Retry and rate limiting behavior

Users don't always get it right the first time. Testing the retry path is worth including in your test suite:

  • Handling incorrect OTP submissions
  • Limiting the number of attempts
  • Ensuring error messages are clear and consistent

Rate limiting is also worth validating. If a user can request a new OTP indefinitely without any throttling, that's a potential abuse vector. Checking that your backend enforces a reasonable limit on resend requests is a useful security-adjacent test case. A good pattern to test is whether the cooldown period between resend requests is enforced consistently, both through the UI and directly via API.

Error handling and edge cases

Beyond the happy path, real users frequently hit edge cases. These are some of the most important OTP test scenarios to cover:

  • Submitting an expired OTP
  • Submitting a code that was already used
  • Requesting a new code while a previous one is still valid
  • Submitting empty or malformed input
  • Handling cases where the OTP email is never received
Invalid OTP error message

Mapping out all possible states in the OTP flow before writing tests can help ensure full coverage. This makes it easier to validate not just success cases, but also how the system behaves under failure conditions.

Quick checklist: common OTP test cases

If you’re building out your test suite, these are the key OTP test cases to cover:

Common OTP test cases

6. How to automate email OTP testing in CI/CD

Automating email OTP testing involves three parts: a browser automation framework to drive the UI, a programmable inbox to receive the OTP email, and an API to fetch and extract the code programmatically.

The example below uses Cypress and testmail.app, but the same approach applies to any end-to-end (E2E) testing framework.

Prerequisites

  • A testmail.app account (API key and namespace from the console)
  • Node.js and npm installed
  • Cypress installed in your project (npm install cypress --save-dev)
  • Axios and chance installed in your project (npm install axios chance --save-dev)

Configure your environment

Store your testmail.app credentials in a cypress.env.json file at the root of your project. Avoid hardcoding these in your test files.

{
  "TESTMAIL_APIKEY": "YOUR_API_KEY",
  "TESTMAIL_NAMESPACE": "YOUR_NAMESPACE"
}

Your test email follows this format:
[email protected]

The tag can be anything and doesn’t need to be pre-created. Using a unique tag per test run keeps inboxes isolated and prevents emails from previous runs from interfering with your automated OTP tests.

Write the test

This example covers a full end-to-end OTP testing flow. The test visits the login page, submits an email address to trigger the OTP, fetches the email via the testmail.app API using livequery, extracts the code, submits it, and asserts a successful outcome.

Add a task to fetch the OTP email in cypress.config.js:

const axios = require('axios').default;

module.exports = {
  e2e: {
    taskTimeout: 300000, // 5 min — livequery uses 307 redirects every 60s

    setupNodeEvents(on) {
      on('task', {
        async fetchOTPEmail({ ENDPOINT, TAG, startTimestamp }) {
          const response = await axios.get(ENDPOINT, {
            params: {
              tag: TAG,
              timestamp_from: startTimestamp,
              livequery: true,
            },
          });

          const email = response.data.emails?.[0];
          if (!email) throw new Error(`No email arrived for tag "${TAG}".`);

          // Adjust this regex to match your app's OTP format
          const match = email.text.match(/\b\d{6}\b/);
          if (!match) throw new Error('OTP not found. Check your regex and email format.');
          return match[0];
        },
      });
    },
  },
};

Then write the test spec:

const ChanceJS = require('chance');
const chance = new ChanceJS();

describe('Email OTP login', () => {
  it('should log in successfully using an email OTP', () => {
    const NAMESPACE = Cypress.env('TESTMAIL_NAMESPACE');
    const APIKEY = Cypress.env('TESTMAIL_APIKEY');
    const TAG = chance.string({ length: 12, pool:'abcdefghijklmnopqrstuvwxyz0123456789' });
    const TESTEMAIL = `${NAMESPACE}.${TAG}@inbox.testmail.app`;
    const ENDPOINT = `https://api.testmail.app/api/json?apikey=${APIKEY}&namespace=${NAMESPACE}`;
    const startTimestamp = Date.now();

    cy.visit('https://your-app.com/login');
    cy.get('input[name="email"]').type(TESTEMAIL);
    cy.get('button[type="submit"]').click();

    cy.task('fetchOTPEmail', { ENDPOINT, TAG, startTimestamp }).then((otpCode) => {
      cy.get('input[name="otp"]').type(otpCode);
      cy.get('button[type="submit"]').click();
      cy.url().should('include', '/dashboard');
    });
  });
});

Handling timing and extraction

  • timestamp_from ensures only new emails are fetched (avoids stale matches)
  • livequery: true waits until the email arrives instead of polling repeatedly
  • Regex like /\b\d{6}\b/ extracts a standard OTP — adjust this for your format

Since live queries can wait indefinitely, set a timeout (the example above uses 5 minutes) to avoid tests hanging if no email arrives.

Running OTP tests in CI/CD

The setup above works in CI/CD with minimal changes. A few things to keep in mind when running automated email OTP tests in a pipeline:

Store credentials securely
Store your testmail.app API key and namespace as environment secrets in your CI provider instead of committing them to your repository.

For example, in GitHub Actions:

- name: Run Cypress tests
  env:
    CYPRESS_TESTMAIL_APIKEY: ${{ secrets.TESTMAIL_APIKEY }}
    CYPRESS_TESTMAIL_NAMESPACE: ${{ secrets.TESTMAIL_NAMESPACE }}
  run: npx cypress run

Use isolated inboxes for parallel tests
If you’re running tests in parallel, generate a unique random tag to ensure that each run uses a clean inbox:

const ChanceJS = require('chance');
const TAG = new ChanceJS().string({ length: 12, pool: 'abcdefghijklmnopqrstuvwxyz0123456789' });
const TESTEMAIL = `${NAMESPACE}.${TAG}@inbox.testmail.app`;

Account for delivery delays
Email delivery in CI can be slower than in local environments. Setting a slightly higher timeout (the example above uses 5 minutes) on your live query helps avoid intermittent failures caused by delays rather than actual bugs.

Other frameworks

The same automated OTP testing pattern applies across frameworks:

  1. Generate a test email address
  2. Trigger the OTP flow
  3. Fetch the email via API
  4. Extract the OTP
  5. Submit and validate

The testmail.app API works with any HTTP client. For framework-specific walkthroughs:

7. Common OTP testing issues (and how to fix them)

Even with a solid setup, a few issues show up regularly in email OTP testing. Most are easy to fix once you know where to look.

OTP email never arrives in the test inbox

This is one of the most common OTP testing issues. It usually comes down to one of three causes: the email is sent to the wrong address, it’s filtered as spam, or delivery is delayed.

Start by verifying that the test email address is passed correctly. If that looks fine, check spam folders — staging environments often have misconfigured SPF, DKIM, or DMARC. If tests are timing out intermittently, increase your live query timeout. If the email still doesn’t arrive, check whether it’s being sent at all or being delayed by your email provider.

Test picks up an email from a previous run

This happens when tests share an inbox or don’t filter emails correctly. Always use a unique tag or inbox per test run and filter emails based on when the test started (for example, using a timestamp). This ensures your test only picks up fresh emails and avoids cross-test interference — a common cause of flaky OTP tests in CI/CD.

OTP extraction fails or returns the wrong value

If your test can’t extract the code or returns the wrong value, the issue is usually with the regex pattern.

A pattern like /\b\d{6}\b/ works for a 6-digit numeric OTP, but will fail for other formats. Make sure your extraction logic matches your actual OTP format, and test it against both HTML and plain text email bodies. Also, check that there aren’t multiple matching values in the email.

OTP expires before submission

This usually means too much time is passing between generation and validation. Using a live query approach instead of polling helps reduce delays. If expiry still causes failures, review your expiry window and ensure it accommodates typical delivery times in CI/CD environments.

Tests are flaky in CI/CD

If tests pass locally but fail in CI/CD, it’s almost always due to timing or shared state. Email delivery is often slower in CI, especially under load. Increasing timeouts is a good first step. If issues persist, check for shared inboxes across parallel runs, which can cause interference even when filtering is applied.

OTP rate limiting during test runs

If your app throttles OTP requests by IP or time window, tests can fail at the triggering step rather than the email step, which makes it harder to diagnose.

Introduce a short delay between runs, or configure your staging environment to apply more relaxed rate limits for test traffic. It’s also worth keeping your email testing tool’s API rate limits in mind when running large parallel test suites — checking limits before scaling helps avoid unexpected failures.

OTP reuse and invalidation issues

Two related issues worth testing together. First, after a successful submission, attempting to reuse the same code should be rejected — if it isn't, single-use enforcement is missing server-side. Second, if a user requests a new OTP while a previous one is still valid, the old code should be invalidated. A simple test for both: submit an OTP successfully, then attempt to submit the same code again, and assert it's rejected. Also, request a second OTP and assert the first one no longer works.

8. FAQs: Frequently asked questions about OTP email testing

1. How do you automate OTP email testing in CI/CD pipelines?

Automating email OTP testing in CI/CD involves generating a unique test email, triggering the OTP flow, and using an email API to wait for and retrieve the message. The OTP is then extracted and submitted to complete the flow. Tools like testmail.app simplify this by providing programmable inboxes designed for automated OTP testing and CI/CD workflows.

2. How can I read and extract OTP codes from emails in automated tests?

You can retrieve OTP emails using an email testing API tied to a unique inbox or identifier. Most email testing tools provide parsed email content (subject, plain text, and HTML), making it easy to locate the OTP without manually processing raw messages. Once retrieved, the OTP can be extracted using a regex or selector from the structured content and used to validate authentication flows programmatically.

3. How do I wait for an email in tests without using fixed delays?

Instead of fixed delays, use an email testing API that can wait or poll for incoming messages. The test pauses until the expected email arrives or a timeout is reached. This reduces flaky tests and is more reliable than using sleep-based waits. Tools like testmail.app provide built-in wait-for-email functionality for this.

4. Why are my OTP or email tests flaky, and how can I fix them?

Email tests often become flaky due to delays, shared inboxes, or timing issues. To fix this, use unique email addresses per test, rely on API-based waiting instead of timeouts, and avoid reusing inboxes across parallel runs. This ensures consistent and isolated test execution.

5. What’s the difference between email testing APIs and temporary email services?

Temporary email services are designed for manual, disposable use. In contrast, email testing APIs are built for automation. APIs allow you to programmatically receive, search, and parse emails, making them suitable for CI/CD and end-to-end testing. Temporary inboxes typically lack the reliability and features needed for automated OTP testing.

Subscribe to blog

Stay updated with our latest insights and curated articles delivered straight to your inbox.