How to Test OTP Flow in Mobile Apps (Practical Guide)
One-time passwords look simple — generate a code, deliver it, user types it, done. In practice, OTP is one of the most failure-prone flows in a mobile app. SMS delivery is not guaranteed. Auto-fill be
One-time passwords look simple — generate a code, deliver it, user types it, done. In practice, OTP is one of the most failure-prone flows in a mobile app. SMS delivery is not guaranteed. Auto-fill behaves differently across OS versions. Resend logic gets abused. Expiry windows are mis-tuned. This guide is the full test matrix I use before signing off any OTP feature.
Why OTP is fragile
Three systems have to line up: your server (generates and validates), your delivery provider (SMS gateway or email), and the client (receives, displays, submits). Each can fail independently. The client has least control and most responsibility.
What to test
Happy path
- Request OTP → receive within 10 seconds → enter correctly → authenticated
- Paste OTP from SMS → auto-submit if complete → authenticated
- Android auto-fill from SMS → field populates → user confirms
Resend behavior
- Resend enabled after timer (30s or 60s) — button disabled before that
- Three resends burn through quota — lockout message or CAPTCHA
- Resend invalidates previous OTP — entering the old one now fails
Expiry
- Wait 2 minutes, try the OTP — rejected with clear message
- Wait 10 minutes, try — rejected (backend must enforce, not just UI)
Validation
- Wrong OTP — error shown, attempt counter increments
- Three wrong attempts — lockout or back to request step
- Correct OTP with trailing space from paste — trimmed or rejected
- OTP with non-digit characters — input field filters them out
Edge cases
- OTP screen backgrounded for 5 minutes — still works if not expired
- Rotation during OTP entry — value preserved
- Keyboard switching (numeric to alpha) — does not clear value
- OTP received via email vs SMS — both flows tested
- Concurrent login attempts from two devices — which OTP wins?
- Device offline when OTP submitted — queued or error?
- Deep link from SMS — opens app directly to OTP entry with code pre-filled
Accessibility
- Each digit field has proper
contentDescription("OTP digit 1 of 6") - Screen reader announces received OTP if auto-filled
- Paste works with Switch Access or Voice Access enabled
Manual testing approach
Get a real device. Use a real phone number. Get the OTP SMS on the same device and test auto-fill. Repeat with a second device for the receive-elsewhere flow. Test with a VoIP number (Google Voice) — many delivery providers silently drop these, you should know if yours does.
Deliberately wait past expiry. Request OTP, set a timer, come back. Verify the backend actually invalidates (some apps have UI expiry but accept expired OTPs at the API layer — real security bug).
Automated testing
Mock the SMS gateway in staging. Use a fixed test OTP (e.g., 123456) for accounts tagged as test users. Real SMS is expensive and flaky for CI.
Appium example
def test_otp_happy(driver, otp_code):
driver.find_element(AppiumBy.ID, "phone_input").send_keys("+15551234567")
driver.find_element(AppiumBy.ID, "send_otp").click()
WebDriverWait(driver, 15).until(EC.presence_of_element_located((AppiumBy.ID, "otp_digit_1")))
for i, digit in enumerate(otp_code, 1):
driver.find_element(AppiumBy.ID, f"otp_digit_{i}").send_keys(digit)
WebDriverWait(driver, 10).until(EC.presence_of_element_located((AppiumBy.ID, "home")))
For TOTP (time-based, no SMS), use pyotp to generate the expected code from the test secret:
import pyotp
otp = pyotp.TOTP("JBSWY3DPEHPK3PXP").now()
How SUSA handles this
SUSA detects OTP screens heuristically — short numeric input fields, "verification code" keyword, digit-only keyboards. You pass the OTP at run time with --otp 123456 (for static test OTPs) or --totp-secret (for TOTP). SUSA then types the code digit by digit, handles auto-submit or manual submit, and records whether authentication succeeded.
For apps using Firebase Auth test phone numbers, you can pass the fixed code directly. For real phone numbers in CI, use a third-party SMS receiver service or a Twilio test endpoint — not a real phone.
susatest-agent test myapp.apk --username test@example.com --otp 123456
Common production bugs
- Expiry enforced only client-side — expired OTP accepted at API. Fix: server-side expiry check on every verification.
- OTP auto-fill races manual typing — user types first digit, auto-fill arrives and overwrites. Fix: debounce 300 ms after first keypress.
- Resend button re-enables before server quota resets — user hits 429. Fix: reflect server cool-down in UI.
- Paste from clipboard includes "Your code is" — input field rejects non-digits but does not extract digits from the pasted string. Fix: extract
\d{6}from the pasted text. - OTP sent over email lands in spam — user thinks app is broken. Fix: offer alternative delivery or SMS fallback.
OTP is a flow worth investing an afternoon in for manual + automated + adversarial exploration. The failure modes are subtle and the user impact is total — if OTP breaks, the user cannot sign in.
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