How to Test OTP Verification on Android (Complete Guide)

One‑time password (OTP) flows are a gatekeeper for account creation, password reset, and high‑value transactions. When the OTP path fails, legitimate users cannot sign in, attackers may bypass verific

February 13, 2026 · 15 min read · How-To Guides

Why OTP Verification Matters on Android

Risks in Production

One‑time password (OTP) flows are a gatekeeper for account creation, password reset, and high‑value transactions. When the OTP path fails, legitimate users cannot sign in, attackers may bypass verification, or the app may leak the code through logs or screenshots. In production, a broken OTP flow manifests as spikes in support tickets, abandoned sign‑ups, and potential regulatory penalties if personal data is mishandled. Because the flow touches telephony, network, UI, and backend services, a defect in any layer can surface only under specific device states, carrier behaviors, or OS versions.

Common Failure Modes

Understanding these patterns helps you build a test matrix that catches the same issues before they reach users.

OTP Verification Test Matrix

IDScenarioStepsExpected ResultCategory
1Happy path – correct OTP entered within validity window1. Enter valid phone number 2. Request OTP 3. Receive SMS 4. Input exact 6‑digit code 5. Tap VerifyAccount verified, navigation to next screenFunctional
2OTP expired – user attempts verification after timeout1. Request OTP 2. Wait > expiry (e.g., 120 s) 3. Input code shown in SMS 4. Tap VerifyError message: “Code expired, request a new one”Error handling
3Invalid format – non‑numeric characters entered1. Request OTP 2. Input “12a456” 3. Tap VerifyInline validation: “Please enter only digits”Input validation
4Too few digits – user enters 5 digits1. Request OTP 2. Input “12345” 3. Tap VerifyError: “OTP must be 6 digits”Input validation
5Too many digits – user enters 7 digits1. Request OTP 2. Input “1234567” 3. Tap VerifyError: “OTP must be 6 digits”Input validation
6Leading zeros stripped – OTP “001234” entered as “1234”1. Request OTP that starts with zero(s) 2. Input without leading zeros 3. Tap VerifyError: “Invalid OTP” (server must compare full string)Data handling
7Resend before timeout – user taps Resend twice quickly1. Request OTP 2. Tap Resend after 2 s 3. Tap Resend again after 2 s 4. Verify second OTPOnly the most recent OTP is valid; first OTP rejectedRate limiting
8Network loss after request – device goes offline before SMS arrives1. Request OTP 2. Enable airplane mode 3. Wait for SMS (never arrives) 4. Disable airplane mode 5. Input any codeTimeout UI: “Unable to verify, check connection”Resilience
9Dual‑SIM device – SMS arrives on non‑default SIM1. Set phone number to SIM 2 2. Request OTP 3. Verify SMS received on SIM 2 4. Input codeVerification succeeds regardless of default SIM settingMulti‑SIM
10Carrier‑specific formatting – SMS includes extra text before code1. Request OTP 2. Receive SMS: “Your OTP is: 654321 – do not share” 3. Extract “654321” 4. Input codeVerification succeeds; app must ignore surrounding textParsing
11Accessibility – TalkBack user navigates to OTP field1. Enable TalkBack 2. Swipe to OTP input 3. Double‑tap to edit 4. Enter code via keyboard 5. Tap VerifyTalkBack announces field label, input changes, and success/error messagesAccessibility
12Color contrast – OTP field label uses low‑contrast gray on white1. Inspect UI with accessibility scanner 2. Verify contrast ratio ≥ 4.5:1Label meets WCAG AA contrastVisual
13Touch target – OTP button smaller than 48 dp1. Measure button size with layout inspector 2. Verify ≥ 48 dp × 48 dpButton passes touch target guidelineAccessibility
14Security – OTP appears in notification preview on lock screen1. Request OTP 2. Lock device 3. Check notification shadeNo OTP visible in preview; only generic “New message”Privacy
15Security – OTP logged to Logcat via verbose tag1. Request OTP 2. Filter Logcat for app tag 3. Verify no OTP string appearsNo OTP in logsData protection
16Backend race – two verification requests with same phone number sent within 500 ms1. Initiate two OTP requests almost simultaneously 2. Receive two distinct codes 3. Attempt to verify with first code 4. Attempt to verify with second codeOnly the latest code validates; earlier code rejectedConcurrency
17Doze mode – device enters deep sleep after OTP request1. Request OTP 2. Leave device idle until Doze activates 3. Wait for SMS (arrives while device asleep) 4. Wake device 5. Input codeVerification succeeds; alarm manager or WorkManager delivers SMS broadcastPower management
18SIM change during flow – user swaps SIM after requesting OTP1. Request OTP on SIM A 2. Remove SIM A, insert SIM B 3. Receive OTP on SIM B (if number ported) 4. Input codeFlow fails gracefully with “Number changed, request again”SIM management
19International number – OTP sent via SMS to +44 number1. Enter international format 2. Request OTP 3. Receive SMS 4. Input codeVerification works; app must not strip ‘+’ or leading zerosInternationalization
20User aborts – taps back button before entering OTP1. Request OTP 2. Press system back 3. Verify app returns to previous screenNo OTP request remains active; no leaked tokenNavigation

How to use the matrix

Manual Testing Approach

Environment Setup

  1. Device selection – Use at least one physical phone running Android 11, one running Android 13, and a tablet or foldable to capture UI variations.
  2. SIM configuration – Insert active SIMs capable of receiving SMS; for dual‑SIM tests, provision two numbers from different carriers.
  3. Network tools – Enable Wi‑Fi, cellular data, and airplane mode toggles via Settings → Network & Internet. Keep a USB‑connected PC with adb installed for log capture.
  4. Mock SMS gateway – If you want to avoid real carrier charges, set up a local SMS emulator (e.g., Android Emulator’s extended controls → Telephone → Send an SMS) or use a service like Twilio with a webhook that forwards messages to the device via push notification.
  5. Logging – Run adb logcat -v time > otp_log.txt before starting the test; filter later with grep -i "otp\|verification\|sms" to isolate relevant lines.

Step‑by‑Step Manual Test Procedure

  1. Pre‑condition – Ensure the app is in a clean state (clear data/storage via Settings → Apps → [YourApp] → Storage → Clear Data).
  2. Launch flow – Navigate to the OTP screen (e.g., tap “Sign up → Verify phone”).
  3. Request OTP – Tap the “Send code” button. Observe the UI for a loading indicator and a toast confirming the request.
  4. Capture SMS
  1. Input OTP – Tap each digit field (or the single OTP box) and type the code. Verify that the keyboard shows numeric‑only layout if the app enforces it.
  2. Submit – Press the Verify button.
  3. Validate outcome
  1. Repeat error paths – For each matrix row, modify the input or timing as described, then observe the UI and logs.
  2. Accessibility check – Turn on TalkBack, navigate to the OTP field using swipe gestures, and confirm that the spoken label matches the visual label and that error messages are announced.
  3. Privacy check – After receiving the SMS, lock the device and view the notification shade; ensure the OTP digits are not visible.

Tools for Manual Verification

Automated Testing Approaches

Unit and Integration Tests with Espresso

Espresso shines for verifying UI logic without needing a real SMS gateway. Use a dependency‑injection framework (e.g., Hilt) to provide a fake OtpRepository that returns a predetermined code.


@RunWith(AndroidJUnit4::class)
class OtpVerificationTest {

    @get:Rule
    val instantTaskRule = InstantTaskExecutorRule()

    @Mock
    private lateinit var otpRepository: OtpRepository

    @Before
    fun setUp() {
        MockitoAnnotations.initMocks(this)
        // Replace the repository in the DI graph with the mock
        // (example using Hilt test bindings)
    }

    @Test
    fun validOtpNavigatesToHome() {
        // Given
        `when`(otpRepository.requestOtp(any())).thenReturn(Completable.complete())
        `when`(otpRepository.verifyOtp(eq("123456"))).thenReturn(Single.just(User()))  

        // When
        launchActivity<OtpActivity>()
        onView(withId(R.id.btn_send_code)).perform(click())
        onView(withId(R.id.otp_input)).perform(replaceText("123456"), closeSoftKeyboard())
        onView(withId(R.id.btn_verify)).perform(click())

        // Then
        onView(withId(R.id.nav_home)).check(matches(isDisplayed()))
    }

    @Test
    fun expiredOtpShowsError() {
        `when`(otpRepository.verifyOtp(eq("654321"))).thenReturn(
            Single.error(OtpException.Expired)
        )
        launchActivity<OtpActivity>()
        onView(withId(R.id.btn_send_code)).perform(click())
        onView(withId(R.id.otp_input)).perform(replaceText("654321"), closeSoftKeyboard())
        onView(withId(R.id.btn_verify)).perform(click())
        onView(withId(R.id.otp_error)).check(matches(withText(containsString("expired"))))
    }
}

UI Automator for Cross‑App Flows

When the OTP arrives via the native SMS app, UI Automator can switch context to retrieve the code.


public String fetchOtpFromSmsUi() throws UiObjectNotFoundException {
    // Open notification shade
    UiDevice device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
    device.openNotification();
    // Wait for the SMS notification (adjust timeout as needed)
    UiObject2 notif = device.wait(Until.findObject(By.textContains("Your OTP is")), 5000);
    if (notif == null) throw new NoSuchElementException("SMS notification not found");
    // Expand notification to see full text
    notif.click();
    UiObject2 expanded = device.wait(Until.findObject(By.clazz(TextView.class)), 3000);
    String text = expanded.getText();
    // Extract 6‑digit number using regex
    Matcher m = Pattern.compile("\\b\\d{6}\\b").matcher(text);
    return m.find() ? m.group() : null;
}

Integrate this helper into an Espresso test: after clicking “Send code”, call fetchOtpFromSmsUi(), then input the returned string. This validates the end‑to‑end path, including the notification shade interaction.

Mock Servers for SMS/Otp Simulation

Tools like WireMock or MockServer can emulate an HTTP‑based OTP API (if your backend uses REST to trigger SMS).


# Start WireMock on port 8080
java -jar wiremock-standalone.jar --port 8080
# Define a stub that returns a fixed OTP ID
curl -X PUT http://localhost:8080/__admin/mappings/new \
  -H "Content-Type: application/json" \
  -d '{
        "request": {
          "method": "POST",
          "urlPath": "/otp/request",
          "bodyPatterns": [ { "matchesJsonPath": "$[?(@.phoneNumber == \"+15551234567\")]" } ]
        },
        "response": {
          "status": 200,
          "jsonBody": { "otpId": "abc123", "expiresIn": 120 }
        }
      }'

Your test suite can point the app’s base URL to http://10.0.2.2:8080 (the emulator’s alias for host localhost) and rely on the stub to deliver deterministic responses. This approach eliminates carrier latency and lets you simulate network failures by returning 500 or delaying the response.

CI Integration

Edge Cases That Appear Only in Production

Network Latency and Retry

In a lab, the OTP request often resolves in < 200 ms. In the field, congested cellular networks can add 2‑4 seconds of latency, causing the UI to show a spinner that disappears before the server response arrives. If the app mistakenly treats the timeout as a failure, users see an error even though the SMS is still en route. Mitigation: implement a retry with exponential backoff and keep the “Resend” button disabled until the timeout elapses.

Carrier‑Specific SMS Formatting

Some carriers prepend service messages (e.g., “FreeMsg:”) or append advertising footers. If your extraction logic assumes the OTP is the first or last token, you may miss the code. Use a regex that searches for \b\d{6}\b anywhere in the body, and log the raw SMS for carriers that deviate.

SIM Swap and Number Recycling

An attacker who convinces a carrier to port the victim’s number can receive OTPs intended for the legitimate user. While this is a carrier‑side issue, your app can reduce risk by:

Doze Mode and Battery Optimizations

Android 6+ places apps in Doze after prolonged idle periods, delaying alarm‑based listeners. If your OTP request relies on AlarmManager to wake a BroadcastReceiver that reads the SMS, the callback may be postponed until the device exits Doze, leading to a perceived “missing code”. Use setExactAndAllowWhileIdle or a WorkManager with setExpedited(true) for time‑critical OTP handling.

Multiple SIM Devices

On dual‑SIM phones, the telephony manager may default to SIM 1 for outgoing data but deliver SMS to SIM 2. If your app reads the phone number from TelephonyManager.getLine1Number() without checking the active data SIM, you may send the OTP request to the wrong number. Retrieve the subscription ID associated with the SMS via SmsManager.getSmsForPackage or use the subscription ID provided by the incoming SMS intent.

Accessibility Considerations Beyond TalkBack

Autonomous, Persona‑Driven Exploration with SUSA

How SUSA Models Different Users

SUSA uploads an APK (or points at a web URL) and then launches a set of simulated personas, each with a distinct interaction profile:

PersonaBehavior TraitsTypical OTP‑related Actions
CuriousTaps every visible element, explores hidden menusMay long‑press the OTP field to trigger paste, attempts to drag the field, tries to use voice input
ImpatientRapid taps, minimal waitingSpams the “Send code” button, attempts to verify before the SMS arrives, cancels the flow quickly
NoviceRelies on labels and hints, avoids gesturesReads the hint text, uses the auto‑fill suggestion bar, may struggle with the numeric‑only keyboard
AdversarialAttempts to break validation, injects malformed dataEnters non‑numeric strings, extremely long inputs, attempts SQL‑like payloads in the OTP field
ElderlySlower interaction, prefers larger touch targetsUses the accessibility zoom gestures, may miss the small resend timer, benefits from voice feedback
Power userUtilizes shortcuts, copy/paste, expects efficiencyPastes OTP from clipboard, uses the “Copy” action on the notification, expects the app to auto‑detect the code

Each persona drives the app through a state‑machine that records UI events, network calls, and system broadcasts. The platform builds a graph of visited screens and flags any transition that leads to a crash, ANR, dead end, or accessibility violation.

What It Finds That Scripts Miss

Traditional automated scripts follow a deterministic path: request OTP → wait fixed delay → input code → verify. They never:

SUSA’s cross‑session learning means that after a first run it remembers which UI elements produced no useful outcome (e.g., a button that only shows a toast) and avoids re‑tapping them in subsequent runs, focusing effort on unexplored areas like the “Help” link that opens a webview with a poorly formatted OTP example.

Example Session Trace (condensed)


[00:00] Persona: Curious
[00:01] Tap: btn_send_code → Network POST /otp/request (200)
[00:02] Swipe down notification shade → Long‑press on SMS notification → Paste dialog appears
[00:03] Paste → Otp field receives " 123456 " (note surrounding spaces)
[00:04] Tap: btn_verify → Network POST /otp/verify (400) → Error: "Invalid OTP"
[00:05] Logcat: W/OtpViewModel: Received OTP with leading/trailing whitespace – trimmed before send
[00:06] Persona: Impatient
[00:07] Tap: btn_send_code (x3 within 800 ms) → Three network requests queued
[00:08] Wait 5 s → Receive three SMS messages (different codes)
[00:09] Tap: btn_verify with first code → 200 OK → Flow completes
[00:10] Note: Server accepted first code despite two pending requests – potential race condition

The trace reveals two issues the script suite would overlook: whitespace handling in pasted OTPs and a race condition where multiple OTP requests are allowed simultaneously. SUSA’s persona engine surfaces these by mimicking real‑world interaction quirks that developers rarely anticipate.

Checklist for OTP Verification Testing

Pre‑Release Checklist

Post‑Release Monitoring

Closing Takeaways

OTP verification may look like a simple “send code, type code, verify” flow, but it sits at the intersection of telephony, UI, security, and accessibility. A robust testing strategy therefore needs:

  1. A comprehensive matrix that spells out happy path, error handling, edge cases, accessibility, and privacy checks.
  2. Manual exploratory steps that use real SIMs, device settings, and accessibility tools to catch carrier‑specific quirks and UI glitches.
  3. Automated checks that isolate logic with Espresso, validate end‑to‑end behavior via UI Automator and mock SMS gateways, and run continuously in CI.
  4. Attention to production‑only realities such as network latency, carrier formatting, Doze mode, dual‑SIM quirks, and SIM‑swap risk.
  5. Periodic autonomous, persona‑driven exploration (using a platform like SUSA) to surface interaction patterns that scripted tests never consider—long‑presses, impatient taps, accessibility gestures, and unexpected interruptions.

By combining these layers, you move from hoping the OTP works in the lab to knowing it works for every user, on every device, under every real‑world condition. Treat OTP verification as a critical security gate, not an after‑thought, and your users will enjoy smoother sign‑ups, fewer support calls, and stronger trust in your app’s safeguards.

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