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
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
- Code never arrives – SMS gateway throttling, incorrect phone number format, or carrier filtering.
- Wrong code accepted – Server‑side validation bugs that compare against a stale token or ignore length checks.
- UI blocks input – Focus loss when the soft keyboard hides the OTP field, or TalkBack announces the wrong element.
- Security leaks – OTP displayed in notification preview, logged to Logcat, or stored in SharedPreferences without encryption.
- Race conditions – User taps “Resend” before the previous request times out, causing duplicate tokens and server‑side confusion.
- Accessibility gaps – Missing content descriptions, insufficient touch target size, or lack of error announcements for visually impaired users.
Understanding these patterns helps you build a test matrix that catches the same issues before they reach users.
OTP Verification Test Matrix
| ID | Scenario | Steps | Expected Result | Category |
|---|---|---|---|---|
| 1 | Happy path – correct OTP entered within validity window | 1. Enter valid phone number 2. Request OTP 3. Receive SMS 4. Input exact 6‑digit code 5. Tap Verify | Account verified, navigation to next screen | Functional |
| 2 | OTP expired – user attempts verification after timeout | 1. Request OTP 2. Wait > expiry (e.g., 120 s) 3. Input code shown in SMS 4. Tap Verify | Error message: “Code expired, request a new one” | Error handling |
| 3 | Invalid format – non‑numeric characters entered | 1. Request OTP 2. Input “12a456” 3. Tap Verify | Inline validation: “Please enter only digits” | Input validation |
| 4 | Too few digits – user enters 5 digits | 1. Request OTP 2. Input “12345” 3. Tap Verify | Error: “OTP must be 6 digits” | Input validation |
| 5 | Too many digits – user enters 7 digits | 1. Request OTP 2. Input “1234567” 3. Tap Verify | Error: “OTP must be 6 digits” | Input validation |
| 6 | Leading zeros stripped – OTP “001234” entered as “1234” | 1. Request OTP that starts with zero(s) 2. Input without leading zeros 3. Tap Verify | Error: “Invalid OTP” (server must compare full string) | Data handling |
| 7 | Resend before timeout – user taps Resend twice quickly | 1. Request OTP 2. Tap Resend after 2 s 3. Tap Resend again after 2 s 4. Verify second OTP | Only the most recent OTP is valid; first OTP rejected | Rate limiting |
| 8 | Network loss after request – device goes offline before SMS arrives | 1. Request OTP 2. Enable airplane mode 3. Wait for SMS (never arrives) 4. Disable airplane mode 5. Input any code | Timeout UI: “Unable to verify, check connection” | Resilience |
| 9 | Dual‑SIM device – SMS arrives on non‑default SIM | 1. Set phone number to SIM 2 2. Request OTP 3. Verify SMS received on SIM 2 4. Input code | Verification succeeds regardless of default SIM setting | Multi‑SIM |
| 10 | Carrier‑specific formatting – SMS includes extra text before code | 1. Request OTP 2. Receive SMS: “Your OTP is: 654321 – do not share” 3. Extract “654321” 4. Input code | Verification succeeds; app must ignore surrounding text | Parsing |
| 11 | Accessibility – TalkBack user navigates to OTP field | 1. Enable TalkBack 2. Swipe to OTP input 3. Double‑tap to edit 4. Enter code via keyboard 5. Tap Verify | TalkBack announces field label, input changes, and success/error messages | Accessibility |
| 12 | Color contrast – OTP field label uses low‑contrast gray on white | 1. Inspect UI with accessibility scanner 2. Verify contrast ratio ≥ 4.5:1 | Label meets WCAG AA contrast | Visual |
| 13 | Touch target – OTP button smaller than 48 dp | 1. Measure button size with layout inspector 2. Verify ≥ 48 dp × 48 dp | Button passes touch target guideline | Accessibility |
| 14 | Security – OTP appears in notification preview on lock screen | 1. Request OTP 2. Lock device 3. Check notification shade | No OTP visible in preview; only generic “New message” | Privacy |
| 15 | Security – OTP logged to Logcat via verbose tag | 1. Request OTP 2. Filter Logcat for app tag 3. Verify no OTP string appears | No OTP in logs | Data protection |
| 16 | Backend race – two verification requests with same phone number sent within 500 ms | 1. Initiate two OTP requests almost simultaneously 2. Receive two distinct codes 3. Attempt to verify with first code 4. Attempt to verify with second code | Only the latest code validates; earlier code rejected | Concurrency |
| 17 | Doze mode – device enters deep sleep after OTP request | 1. Request OTP 2. Leave device idle until Doze activates 3. Wait for SMS (arrives while device asleep) 4. Wake device 5. Input code | Verification succeeds; alarm manager or WorkManager delivers SMS broadcast | Power management |
| 18 | SIM change during flow – user swaps SIM after requesting OTP | 1. Request OTP on SIM A 2. Remove SIM A, insert SIM B 3. Receive OTP on SIM B (if number ported) 4. Input code | Flow fails gracefully with “Number changed, request again” | SIM management |
| 19 | International number – OTP sent via SMS to +44 number | 1. Enter international format 2. Request OTP 3. Receive SMS 4. Input code | Verification works; app must not strip ‘+’ or leading zeros | Internationalization |
| 20 | User aborts – taps back button before entering OTP | 1. Request OTP 2. Press system back 3. Verify app returns to previous screen | No OTP request remains active; no leaked token | Navigation |
How to use the matrix
- Map each ID to a test case in your test management tool.
- Prioritize functional (IDs 1‑5) and error‑handling (6‑9) for every release.
- Run accessibility (11‑13), privacy/security (14‑15), and resilience (16‑18) suites on a rotating basis to catch regressions that only appear under specific device or OS conditions.
Manual Testing Approach
Environment Setup
- Device selection – Use at least one physical phone running Android 11, one running Android 13, and a tablet or foldable to capture UI variations.
- SIM configuration – Insert active SIMs capable of receiving SMS; for dual‑SIM tests, provision two numbers from different carriers.
- Network tools – Enable Wi‑Fi, cellular data, and airplane mode toggles via Settings → Network & Internet. Keep a USB‑connected PC with
adbinstalled for log capture. - 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.
- Logging – Run
adb logcat -v time > otp_log.txtbefore starting the test; filter later withgrep -i "otp\|verification\|sms"to isolate relevant lines.
Step‑by‑Step Manual Test Procedure
- Pre‑condition – Ensure the app is in a clean state (clear data/storage via Settings → Apps → [YourApp] → Storage → Clear Data).
- Launch flow – Navigate to the OTP screen (e.g., tap “Sign up → Verify phone”).
- Request OTP – Tap the “Send code” button. Observe the UI for a loading indicator and a toast confirming the request.
- Capture SMS –
- If using a real SIM, wait for the native SMS notification.
- If using the emulator, open Extended Controls → Telephone → Incoming number → enter the test number → type the 6‑digit code → click Send.
- 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.
- Submit – Press the Verify button.
- Validate outcome –
- Success: app proceeds to the next screen (e.g., profile setup).
- Failure: check that an inline error appears, that the field regains focus, and that any “Resend” timer updates correctly.
- Repeat error paths – For each matrix row, modify the input or timing as described, then observe the UI and logs.
- 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.
- 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
- ADB –
adb shell am start -n com.example.app/.otp.OtpActivityto jump directly to the screen. - Bugreport –
adb bugreport > bugreport.zipfor post‑mortem analysis of ANRs or crashes. - Stetho or Flipper – Inspect network calls in real time to confirm the OTP request payload and response.
- Accessibility Scanner – Install from Play Store, run on the OTP screen, and note any contrast or touch‑target warnings.
- GTracks – A lightweight overlay that shows touch coordinates; useful for verifying that the OTP button respects the 48 dp minimum.
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"))))
}
}
- Why mock the repository? It isolates UI logic from network variability, letting you test edge cases like expired or malformed OTPs instantly.
- Coverage tip – Aim for ≥ 90 % line coverage on the
OtpViewModeland associated use‑classes.
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
- Unit/Espresso – Run on every pull request using GitHub Actions with the
android-emulator-runneraction. - UI Automator + Mock SMS – Execute in a nightly job on a fleet of real devices (Firebase Test Lab or AWS Device Farm) to capture device‑specific quirks.
- Artifact archiving – Store
logcatscreenshots and video recordings from the emulator for debugging flaky tests.
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:
- Requiring re‑authentication (password or biometric) before accepting an OTP for a high‑value action.
- Detecting sudden changes in device ID or SIM serial between OTP request and verification, then prompting for additional verification.
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
- Switch Access – Users who navigate with external switches need larger hit areas and predictable focus order. Verify that moving focus from the “Send code” button to the OTP input does not skip intermediate elements.
- Font Scaling – Test with the system font size set to Largest; ensure the OTP field does not truncate or overlap neighboring views.
- Color Blindness – Avoid relying solely on red/green to indicate error/success; pair color changes with icons or text.
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:
| Persona | Behavior Traits | Typical OTP‑related Actions |
|---|---|---|
| Curious | Taps every visible element, explores hidden menus | May long‑press the OTP field to trigger paste, attempts to drag the field, tries to use voice input |
| Impatient | Rapid taps, minimal waiting | Spams the “Send code” button, attempts to verify before the SMS arrives, cancels the flow quickly |
| Novice | Relies on labels and hints, avoids gestures | Reads the hint text, uses the auto‑fill suggestion bar, may struggle with the numeric‑only keyboard |
| Adversarial | Attempts to break validation, injects malformed data | Enters non‑numeric strings, extremely long inputs, attempts SQL‑like payloads in the OTP field |
| Elderly | Slower interaction, prefers larger touch targets | Uses the accessibility zoom gestures, may miss the small resend timer, benefits from voice feedback |
| Power user | Utilizes shortcuts, copy/paste, expects efficiency | Pastes 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:
- Long‑press the OTP field to see if a paste dialog appears and whether the app correctly handles pasted content (including leading zeros).
- Swipe away the notification before the code expires to test whether the app retains the OTP in memory or forces a re‑request.
- Activate TalkBack and then rotate the device to verify that focus is restored correctly after a configuration change.
- Enable airplane mode mid‑flow and then disable it after a long delay to see if the app gracefully handles a delayed SMS.
- **Attempt to test
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
- [ ] All matrix IDs (1‑20) have passing test cases on at least two API levels (e.g., 21 and 33).
- [ ] Espresso unit tests cover ≥ 90 % of ViewModel logic.
- [ ] UI Automator + mock SMS test validates end‑to‑end flow on a real device.
- [ ] Accessibility Scanner reports no contrast or touch‑target violations on the OTP screen.
- [ ] Logcat verification confirms no OTP strings appear in verbose or debug tags.
- [ ] Notification privacy test ensures OTP is never visible on the lock screen.
- [ ] Dual‑SIM test confirms correct SIM selection for both request and receipt.
- [ ] Doze mode test validates that OTP verification succeeds after the device exits idle.
- [ ] SIM swap detection logic (if implemented) triggers a re‑auth prompt.
- [ ] Negative‑input tests (non‑numeric, too short, too long, whitespace) return appropriate inline errors.
- [ ] Respect rate‑limit: “Resend” button remains disabled until the cooldown expires.
Post‑Release Monitoring
- Metric – OTP request success rate (percentage of requests that lead to a verified session). Set alert threshold at 95 %.
- Log – Capture any
OtpExceptioncodes from the backend and correlate with device model, Android version, and carrier. - Crash – Monitor for ANRs on the
OtpVerificationService(if you use a foreground service for SMS retrieval). - User feedback – Tag support tickets with “OTP” and review weekly for emerging patterns (e.g., a specific MVNO consistently failing).
- A/B – If you roll out a new OTP extraction regex, enable a feature flag and compare verification funnels before/after.
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:
- A comprehensive matrix that spells out happy path, error handling, edge cases, accessibility, and privacy checks.
- Manual exploratory steps that use real SIMs, device settings, and accessibility tools to catch carrier‑specific quirks and UI glitches.
- Automated checks that isolate logic with Espresso, validate end‑to‑end behavior via UI Automator and mock SMS gateways, and run continuously in CI.
- Attention to production‑only realities such as network latency, carrier formatting, Doze mode, dual‑SIM quirks, and SIM‑swap risk.
- 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