How to Automate OTP Verification Testing (Step-by-Step)

How to Automate Otp Verification Testing (Step-by-Step) begins with understanding why OTP flows are critical and where automation adds value. One‑time passwords protect account recovery, payment autho

June 12, 2026 · 15 min read · How-To Guides

How to Automate Otp Verification Testing (Step-by-Step) begins with understanding why OTP flows are critical and where automation adds value. One‑time passwords protect account recovery, payment authorization, and device registration, yet they are notoriously flaky to test manually because they depend on external delivery channels (SMS, email, push) and expire quickly. Automating the verification step removes human latency, enables repeatable validation of edge‑case timing, and lets you surface bugs that only appear under load or with specific carrier delays. In this guide you will learn a repeatable process: decide when to automate, pick a framework, craft resilient locators, synchronize with asynchronous OTP arrival, manage test data, embed the checks in CI, and report results. Each section contains concrete code, tables, and checklists you can copy into your repository today.

How to Automate Otp Verification Testing (Step-by-Step): Understanding the Problem Space

OTP verification typically follows this sequence: the user submits an identifier (phone number or email), the backend sends a numeric code via a third‑party, the UI presents an input field, the user enters the code, and the system validates it. Automating this flow requires three moving parts:

  1. Trigger – cause the system to send an OTP (often via API call or UI action).
  2. Capture – retrieve the OTP from its delivery channel before it expires.
  3. Validate – fill the OTP field and submit, then assert the expected post‑condition (e.g., logged‑in state, success toast).

If any part is brittle, the whole test fails. Common failure modes include: SMS gateway latency, email throttling, OTP regeneration on resend, UI changes that hide the input field, and race conditions where the OTP arrives after the test times out. Recognizing these patterns early helps you choose the right synchronization strategy and decide whether to mock the OTP service or use a real‑world capture mechanism.

When to Mock vs. Use Real Delivery

ApproachProsConsTypical Use
Mock OTP service (stubbed API)Instant, deterministic, no external dependenciesDoes not test actual delivery latency or carrier‑specific formattingUnit tests, contract tests, early‑stage feature branches
Real SMS/email via test numbers or sandboxValidates end‑to‑end path, catches provider‑specific bugsVariable latency, possible cost, need for cleanup of used codesPre‑release staging, production‑like smoke tests, compliance verification
Hybrid (mock for most runs, real for nightly)Balances speed and confidenceSlightly more complex CI configurationTeams that run fast feedback loops daily and deeper validation nightly

If your product relies on a single OTP provider with a stable sandbox, start with the real service. If you support multiple carriers or frequently change providers, invest in a thin abstraction layer that can swap between mock and real adapters.

How to Automate Otp Verification Testing (Step-by-Step): When Automation Pays Off

Automation is not free; you must weigh the effort against the return. The following decision matrix helps you decide where to invest.

CriteriaLow Value (Manual OK)Medium Value (Consider Automation)High Value (Automate)
Frequency of execution< once per sprint1‑3 times per sprint≥ daily or per commit
Complexity of OTP flowSimple 1‑field entryMultiple steps (e.g., OTP + password + consent)Flow with retries, fallback channels, or adaptive UI
Failure impactCosmetic UI glitchBlocks core user journey (login, payment)Security‑critical (account takeover, fraud)
Flakiness toleranceHigh (infrequent retries acceptable)Medium (occasional retries okay)Low (must be green on every run)
Team capacityLimited QA bandwidthDedicated automation engineerFull‑stack team with CI maturity

If you score “High” in three or more columns, allocate time to automate. For example, a fintech app that lets users add a bank account via SMS OTP and runs the flow on every pull request clearly belongs in the high‑value bucket.

Cost Estimation Example

Assume a manual tester spends 2 minutes per OTP verification test (including waiting for the SMS). Running the test 50 times per day consumes ~100 minutes of human time. An automated script that runs in 15 seconds (including OTP capture) saves 1.5 hours daily. Over a month, that is ~30 hours saved, which easily justifies the initial 4‑hour investment to write a stable test.

How to Automate Otp Verification Testing (Step-by-Step): Selecting a Test Framework and Tools

Choose a framework that matches your application type (native mobile, hybrid, or web) and your team’s language expertise. Below is a comparison of popular options for OTP verification.

FrameworkLanguageMobile SupportWeb SupportOTP Capture HelpersCommunity Maturity
Appium (Android/iOS)Java, JS, Python, Ruby, C#✅ (real devices/emulators)❌ (webviews only)Custom code to read SMS via Android Telephony API or email via IMAP★★★★★
Selenium WebDriverJava, JS, Python, Ruby, C#❌ (via Selendroid/Appium)Email via IMAP/SMTP, SMS via Twilio test numbers★★★★★
PlaywrightJS/TS, Python, Java, .NET❌ (Chromium/Firefox/WebKit)Built‑in request interception can mock email/SMS APIs★★★★☆
CypressJS/TSCypress‑email plugin, or stub network calls★★★★☆
Robot FrameworkPython/Java (via libraries)✅ (Appium library)✅ (Selenium library)Keywords for IMAP, Twilio, Firebase Test Lab★★★☆☆
SUSA (Susatest) agentCLI, language‑agnostic✅ (autonomous exploration)✅ (URL‑based)Auto‑detects OTP fields, can inject codes via API★★★☆☆

Why the table matters: It lets you match your stack to a tool that already provides OTP‑specific utilities, reducing boilerplate. For instance, if you already use Playwright for web UI tests, you can leverage its page.route feature to intercept the OTP‑delivery endpoint and feed a static code, eliminating reliance on external mail servers.

Selecting Locators First

Regardless of framework, start by inspecting the OTP screen. Look for stable attributes: data-test-id, accessibility-id, or a combination of class and placeholder. Avoid XPath that depends on positional indices (//input[3]) because UI redesigns break them instantly.

#### Example: Android OTP Screen (Jetpack Compose)


<!-- layout snippet -->
<EditText
    android:id="@+id/otp_input"
    android:hint="Enter OTP"
    android:inputType="number"
    android:maxLength="6"
    android:contentDescription="@string/otp_field" />

In Appium (Java) you would locate it as:


By otpField = By.id("otp_input"); // generated from android:id

If the ID is obfuscated, fall back to accessibility:


By otpField = By.accessibilityId("Enter OTP");

#### Example: Web OTP Modal (React)


<div data-testid="otp-modal">
  <input data-testid="otp-code" type="text" autocomplete="one-time-code" />
  <button data-testid="otp-submit">Verify</button>
</div>

Playwright locator:


otp_field = page.get_by_test_id("otp-code")
submit_btn = page.get_by_test_id("otp-submit")

Stable data-testid attributes survive CSS refactors and are readable by both developers and QA.

Building Stable Locators for OTP Screens

Even with good IDs, dynamic content can cause flakiness. Apply these patterns:

  1. Prefix with a feature flag – e.g., data-testid="otp-${featureFlag}" where the flag is constant in test environments.
  2. Use relative locators – locate a nearby static element (like a heading “Verify your phone”) then find the input below it.
  3. Avoid text‑based locators – OTP screens often show localized strings; hardcoding English text fails in other locales.
  4. Combine multiple attributesBy.cssSelector("input[inputmode='numeric'][aria-label='OTP']") is more resilient than a single attribute.

Code: Relative Locator in Appium (Python)


from appium import webdriver
from appium.webdriver.common.mobileby import MobileBy

driver = webdriver.Remote(command_executor='http://127.0.0.1:4723/wd/hub', desired_caps=caps)

# Find the heading that never changes
heading = driver.find_element(MobileBy.ANDROID_UIAUTOMATOR,
                              'new UiSelector().descriptionContains("Verify your phone")')
# Use the heading as an anchor to locate the OTP field below it
otp_field = heading.find_element(MobileBy.XPATH,
                                 './following::android.widget.EditText[1]')

If the heading moves, the test will still fail fast, prompting you to update the anchor rather than chase many broken selectors.

Handling Waits, Synchronization, and Flakiness

OTP arrival is inherently asynchronous. Fixed Thread.sleep calls are brittle and waste CI time. Instead, use explicit waits tied to observable conditions.

Wait for OTP Field to Appear


WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(15));
WebElement otpInput = wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("otp_input")));

Wait for OTP Value to Be Available

When using a real mailbox, poll until a matching code arrives or timeout expires.

#### Python Example Using IMAP (Gmail test account)


import imaplib, email, re, time

def fetch_otp_from_gmail(username, app_password, sender="no-reply@example.com", timeout=60):
    imap = imaplib.IMAP4_SSL("imap.gmail.com")
    imap.login(username, app_password)
    imap.select("inbox")
    start = time.time()
    while time.time() - start < timeout:
        typ, data = imap.search(None, f'(FROM "{sender}" SUBJECT "Your OTP code")')
        for num in data[0].split()[::-1]:  # latest first
            typ, msg_data = imap.fetch(num, "(RFC822)")
            raw = msg_data[0][1]
            msg = email.message_from_bytes(raw)
            body = ""
            if msg.is_multipart():
                for part in msg.walk():
                    if part.get_content_type() == "text/plain":
                        body = part.get_as_string()
                        break
            else:
                body = msg.get_as_string()
            match = re.search(r"\b\d{6}\b", body)
            if match:
                imap.logout()
                return match.group(0)
        time.sleep(5)
    imap.logout()
    raise TimeoutError("OTP not received within timeout")

Integrate this helper into your test:


otp = fetch_otp_from_gmail(test_user, test_app_pwd)
otp_field.send_keys(otp)
submit_btn.click()

Dealing with OTP Resend and Expiration

Some apps allow the user to request a new OTP after a timeout. Your test should handle both scenarios:

  1. First attempt – wait for OTP, submit, assert success.
  2. If failure due to expired code – click “Resend OTP”, repeat the fetch, and retry once.

Encapsulate this logic in a reusable method so each test stays readable.


public boolean verifyWithRetry(String identifier) {
    triggerOtpSend(identifier);
    String otp = fetchOtp();
    fillOtpAndSubmit(otp);
    if (isErrorDisplayed("Code expired")) {
        clickResend();
        otp = fetchOtp(); // fetch again
        fillOtpAndSubmit(otp);
    }
    return isSuccessToastVisible();
}

Reducing Flakiness from Network Variability

Data Setup, Teardown, and Test Data Management

OTP tests often need a pristine user account or a dedicated phone number. Reusing accounts across runs can cause collisions (e.g., OTP sent to a locked account). Adopt one of these strategies:

StrategyDescriptionWhen to Use
Pre‑provisioned test poolCreate a batch of users/numbers before the test suite starts; each test picks an unused entry and marks it as used.Large suites, parallel execution
On‑the‑fly registrationTest signs up a new user via API, then immediately triggers OTP verification.When registration flow is stable and cheap
Temporary aliases (email)Use +tag syntax (testuser+123@example.com) or disposable email services (Mailinator, Guerrilla Mail).Email‑based OTP, low volume
Carrier‑provided test numbersSome SMS gateways offer numbers that always return a predetermined code (e.g., Twilio test credentials).SMS‑based OTP, need deterministic code

Example: Python fixture for on‑the‑fly user creation (REST API)


import pytest, requests, uuid

BASE = "https://api.example.com"

@pytest.fixture
def fresh_user():
    unique = str(uuid.uuid4())[:8]
    payload = {
        "email": f"tester+{unique}@example.com",
        "password": "SecurePass!123",
        "phone": f"+1555{unique:0>7}"
    }
    resp = requests.post(f"{BASE}/users", json=payload)
    resp.raise_for_status()
    user_data = resp.json()
    yield user_data
    # Teardown: delete the user to keep the pool clean
    requests.delete(f"{BASE}/users/{user_data['id']}", headers={"Authorization": f"Bearer {admin_token}"})

In the test:


def test_otp_verification(fresh_user):
    # trigger OTP via login API
    login_resp = requests.post(f"{BASE}/auth/login",
                               json={"email": fresh_user["email"], "password": fresh_user["password"]})
    assert login_resp.status_code == 200
    # OTP will be sent to the email address; fetch it via IMAP as shown earlier
    otp = fetch_otp_from_gmail(fresh_user["email"], test_app_pwd)
    verify_resp = requests.post(f"{BASE}/auth/verify-otp",
                                json={"email": fresh_user["email"], "code": otp})
    assert verify_resp.json()["status"] == "approved"

Cleaning Up OTP Artifacts

After a test finishes, delete any OTP records left in the backend (if your API exposes an endpoint) or simply rely on the user/account teardown to remove associated data. For email‑based OTP, most test mailboxes automatically purge after a few hours; still, consider deleting the fetched message to keep the inbox tidy.

Integrating OTP Verification Tests into CI/CD Pipelines

Automated OTP checks belong in the same pipeline as your other UI or API tests, but you must address two operational concerns: secrets and execution time.

Managing Secrets Securely

Never hardcode test email passwords or Twilio auth tokens in the repository. Use your CI system’s secret store (GitHub Actions secrets, GitLab CI variables, Jenkins Credentials Binding). Retrieve them at runtime:


# .github/workflows/otp-tests.yml
jobs:
  otp-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Set up Python
        uses: actions/setup-python@v4
        with:
          python-version: "3.11"
      - name: Install dependencies
        run: pip install -r requirements.txt
      - name: Run OTP suite
        env:
          GMAIL_USER: ${{ secrets.GMAIL_TEST_USER }}
          GMAIL_APP_PASS: ${{ secrets.GMAIL_TEST_APP_PASS }}
        run: pytest tests/otp_test.py -v

Parallelism and Resource Contention

OTP tests that rely on real SMS or email can saturate external rate limits if run in parallel on many agents. Mitigate by:

Example: GitHub Actions matrix with mock vs real


strategy:
  matrix:
    provider: [mock, real]
    node: [1,2,3]
steps:
  - name: Install
    run: npm ci
  - name: Run tests
    env:
      OTP_PROVIDER: ${{ matrix.provider }}
    run: |
      if [ "$OTP_PROVIDER" = "mock" ]; then
        npm run test:otp:mock
      else
        npm run test:otp:real
      fi

Reporting and Artifacts

Capture screenshots or video on failure to diagnose UI issues. Most frameworks have hooks:

Attach these artifacts to the CI job so developers can view them directly in the UI.

Reporting, Metrics, and Continuous Improvement

A test suite is only as valuable as the feedback it provides. Track the following metrics for OTP verification:

MetricHow to MeasureTarget
Pass rate% of OTP tests green over last 30 runs≥ 95%
Mean time to detect (MTTD)Average time from code commit to failure notification< 5 min
Flakiness indexNumber of retries needed to achieve a stable pass≤ 1 retry per 20 runs
OTP capture latencyTime between OTP send and successful retrieval< 20s (SMS), < 10s (email)
Cost per runEstimated cost of external SMS/email usage< $0.005 per test (if using paid gateway)

Visualization) |

Use a simple JSON ex:metrics

{

"Pass rate" and "Flakiness index" on a dashboard (Grafana, Datadog, or a simple Markdown badge in your README). When flakiness rises, investigate root causes: carrier delays, OTP expiration logic changes, or test data collisions.

Example: Publishing a badge with Shields.io


[![OTP Verification](https://img.shields.io/endpoint?url=https://api.example.com/badge/otp-verification)](https://example.com/dashboard/otp)

The endpoint returns JSON like {"schemaVersion":1,"label":"otp-verification","message":"96% passing","color":"brightgreen"}.

Continuous Improvement Loop

  1. Review failures – triage each OTP test failure; categorize as environment, data, or product bug.
  2. Update locators – if a failure is due to UI change, add a new data-testid or adjust the selector.
  3. Tune waits – increase timeout only after confirming the delay is genuine (e.g., carrier throttling).
  4. Rotate test numbers – if you notice a specific number being blocked by the carrier, replace it.
  5. Automate flakiness detection – add a retry wrapper that logs attempts; after N runs, automatically create a ticket if flakiness exceeds threshold.

Leveraging Autonomous Exploration (SUSA) to Bootstrap OTP Verification Automation

SUSA (Susatest) can explore an app without predefined scripts and discover OTP entry points automatically. When you first integrate SUSA into your workflow, you get a baseline set of flows that include any OTP verification screens the engine encounters during its autonomous crawl.

How It Works

  1. Upload the APK (or provide a web URL).
  2. SUSA launches a set of persona‑driven bots (curious, impatient, power user, etc.).
  3. Each bot interacts with the UI, filling forms, submitting buttons, and handling dialogs.
  4. When the bot detects a numeric input field labeled “OTP”, “Verification Code”, or similar, it logs the screen and attempts to capture the code via a configurable OTP provider (email API, SMS gateway, or a mock endpoint).
  5. The platform generates a regression script in Appium (Android) or Playwright (Web) that replays the discovered flow, complete with waits and assertions.

Benefits for OTP Automation

Practical Steps


# Install the CLI
pip install susatest-agent

# Run exploration against a local APK
susatest explore --app ./myapp-debug.apk \
                 --otp-provider email \
                 --email-user test@example.com \
                 --email-pass $GMAIL_APP_PASS \
                 --output ./generated-tests

The command creates a directory generated-tests containing:

You can then import the Java class into your existing test suite, replace the hardcoded email credentials with CI secrets, and add any additional assertions specific to your business logic (e.g., verifying that a user’s balance updates after OTP verification).

Note: SUSA does not replace handcrafted tests for complex edge cases (e.g., OTP length validation, custom error messages). Use its output as a starting point, then augment with negative scenarios and performance checks.

Checklist and Best Practices Summary

Before you mark an OTP verification test as “ready for CI”, run through this concise list. Each item addresses a common source of flakiness or maintenance overhead.

✅ ItemWhy It Matters
Stable locators – use data-testid, accessibility-id, or combination of attributes.Prevents breakage on UI tweaks.
Explicit waits – wait for element visibility and OTP availability, never Thread.sleep.Reduces wasted time and false negatives.
OTP retrieval timeout – set a realistic upper bound (e.g., 60 s for SMS, 20 s for email) and fail fast if exceeded.Avoids hanging CI jobs.
Retry logic for expiration – handle “code expired” by triggering a resend once.Covers realistic user behavior.
Dedicated test data – use on‑the‑fly accounts, aliases, or a pre‑provisioned pool with cleanup.Prevents data collisions and leakage.
Secret management – store email/SMS credentials in CI secret stores, never in code.Keeps credentials safe.
Parallelism limits – cap concurrent OTP jobs to avoid external rate‑limit throttling.Maintains reliability across runners.
Artifact capture – screenshot/video on failure, log OTP retrieval latency.Speeds up triage.
Metrics tracking – monitor pass rate, flakiness, and latency; alert on degradation.Enables continuous improvement.
Regular review – weekly triage of OTP test failures; update locators, wait times, or test data as needed.Keeps the suite trustworthy over time.

Quick Refactor Example: Moving from Sleep to Wait

Before (flaky):


Thread.sleep(8000); // hope OTP arrives
otpField.sendKeys(otpCode);

After (robust):


WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(20));
wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("otp_input")));
otpField.sendKeys(otpCode);

The explicit wait adapts to actual OTP arrival time, cutting unnecessary idle time when the code comes early and extending when the network is slow.

Closing Takeaways

Automating OTP verification is not a luxury; it is a necessity for any product that relies on two‑factor authentication for security or core user flows. Start by quantifying the value: if the test runs more than once per sprint, protects a security‑critical path, or suffers from high manual effort, invest in automation. Choose a framework that matches your stack and offers built‑in helpers for OTP capture—Playwright for web, Appium for native, or a hybrid approach if you need both. Build locators that survive redesigns, rely on explicit waits tied to observable conditions, and manage test data with a clean‑up strategy to avoid collisions. Integrate the checks into your CI pipeline using secret stores, controlled parallelism, and artifact capture for fast feedback. Leverage tools like SUSA’s autonomous explorer to generate a baseline test suite quickly, then enrich it with negative cases and performance checks. Finally, treat the OTP test suite as a living instrument: monitor pass rate, flakiness, and latency, and act on regressions before they erode confidence in your releases.

By following the step‑by‑step process outlined here, you will turn a notoriously fragile manual checkpoint into a reliable, automated gate that ships faster, catches regressions earlier, and keeps your users’ accounts safe.

Test Your App Autonomously

Upload your APK or URL. SUSA explores like 10 real users — finds bugs, accessibility violations, and security issues. No scripts.

Try SUSA Free