How to Test OTP Verification on iOS (Complete Guide)
How to Test Otp Verification on iOS (Complete Guide)
How to Test Otp Verification on iOS (Complete Guide)
Testing one‑time passcode (OTP) verification on iOS is a critical quality gate because it sits at the intersection of authentication, network reliability, and user experience. A broken OTP flow can lock out legitimate users, expose weak validation logic, or create frustrating friction that drives churn. This guide walks you through why OTP verification matters, the ways it fails in production, a exhaustive test matrix, manual and automated techniques, accessibility and security considerations, and how autonomous persona‑driven exploration surfaces bugs that scripted tests miss. By the end you will have a concrete checklist, ready‑to‑copy code samples, and a mental model for building reliable OTP validation in any iOS product.
Why OTP Verification Matters on iOS Apps
OTP verification is often the final step in account creation, password reset, or high‑value transaction approval. Unlike static passwords, OTPs are time‑bound and delivered via SMS, push notification, or email, which introduces asynchronous delivery, carrier latency, and user‑interaction variables. When the verification screen misbehaves, users cannot proceed, leading to abandoned sign‑ups or failed payments. From a compliance standpoint, many regulations (PSD2, GDPR, CCPA) require strong customer authentication; a flawed OTP implementation can violate those rules and attract penalties. Moreover, OTP screens are frequently targeted by attackers attempting brute‑force, replay, or SIM‑swap attacks, making security testing inseparable from functional testing.
Common Failure Modes in Production
Understanding where OTP verification breaks helps you prioritize test cases. The most frequent production issues fall into these buckets:
| Failure Category | Typical Symptom | Root Cause |
|---|---|---|
| Delivery Delay | OTP never arrives within the UI timeout | Network latency, carrier filtering, or APNs push throttling |
| Incorrect Length Handling | UI rejects valid 6‑digit code but accepts 5‑digit | Hard‑coded length checks or off‑by‑one bugs |
| Auto‑Fill Misbehaviour | iOS AutoFill populates wrong field or overwrites user input | Mis‑associated UITextContentType or missing .textContentType = .oneTimeCode |
| Race Condition | Verification succeeds before network response, then fails on retry | UI proceeds on local validation without awaiting server confirmation |
| Accessibility Gap | VoiceOver reads “secure text field” but announces no hint for OTP entry | Missing accessibilityLabel or accessibilityHint |
| Localization Bug | OTP prompt shows English despite device set to another language | Hard‑coded strings or missing .stringsdict entries |
| Security Bypass | Re‑using a previously valid OTP after expiration | Server‑side timestamp check missing or client‑side replay allowed |
| Crash on Interruption | App terminates when user switches to Messages app during OTP entry | Improper handling of UIApplicationWillResignActive notification |
Each of these symptoms can be reproduced in a controlled test environment, but some (e.g., carrier filtering) require special tooling or external services. The test matrix below captures both deterministic and nondeterministic variations.
Building a Comprehensive Test Matrix
A thorough OTP verification test matrix covers happy paths, error paths, edge cases, accessibility, localization, and security. Use the table as a master checklist; tick each item as you automate or manually verify.
Test Matrix Table
| ID | Category | Sub‑category | Description | Expected Result | Automation Feasibility |
|---|---|---|---|---|---|
| H1 | Happy Path | Correct OTP | User receives valid OTP, enters it, taps Verify | Account verified, navigation proceeds | High |
| H2 | Happy Path | AutoFill | System suggests OTP from QuickType bar, user taps to fill | Field populated correctly, Verify enabled | Medium (requires iOS 12+ device) |
| H3 | Happy Path | Paste | User copies OTP from notification and pastes into field | Field shows pasted code, Verify enabled | High |
| E1 | Error Path | Wrong OTP | User enters incorrect 6‑digit code | Error message displayed, field remains enabled, retry allowed | High |
| E2 | Error Path | Expired OTP | OTP generated >5 min ago (or server‑side TTL) entered | Error: “Code expired”, option to resend | High |
| E3 | Error Path | Malformed Input | User enters non‑numeric characters, too few/too many digits | Field rejects input, shows inline validation | High |
| E4 | Error Path | Network Failure | Simulated loss of connectivity after OTP entry | UI shows “Unable to verify, check connection”, no crash | Medium (requires network conditioning) |
| E5 | Error Path | Server 500 | Backend returns internal error | Generic error shown, retry button available | Medium |
| EC1 | Edge Case | Rapid Re‑send | User taps Resend OTP three times within 2 seconds | Only one request sent, UI shows cooldown timer | Medium |
| EC2 | Edge Case | App Background | User switches to Messages app to read OTP, returns after 30 s | OTP field retains entered digits, Verify still works | High |
| EC3 | Edge Case | Interruption Alert | System presents a permission alert while OTP field is first responder | Alert dismissed, OTP field regains focus, no data loss | Medium |
| EC4 | Edge Case | VoiceOver Navigation | VoiceOver user navigates to OTP field, hears hint, enters via keyboard | Field accessible, input announced correctly | High (requires accessibility testing) |
| EC5 | Edge Case | Right‑to‑Left Layout | Device language set to Arabic/Hebrew, OTP field mirrors correctly | Layout flips, input direction respects RTL | Medium |
| SEC1 | Security | Brute‑Force Limit | Attempt 10 wrong OTPs in quick succession | Account locked or rate‑limited, user sees “Too many attempts” | Low (needs backend stub) |
| SEC2 | Security | Replay Attack | Capture a valid OTP, re‑use after 2 min | Server rejects as expired/replayed | Low |
| SEC3 | Security | SIM‑Swap Detection | OTP sent to a number that changed carriers recently | Optional: backend flags number change, UI shows warning | Low (depends on backend) |
| SEC4 | Privacy | No OTP in Logs | Verify that OTP never appears in console or crash logs | OTP absent from NSLog, os_signpost, or third‑party analytics | High (via log capture) |
| ACC1 | Accessibility | Dynamic Type | User selects largest accessibility text size | OTP field and labels scale correctly, no clipping | High |
| ACC2 | Accessibility | Reduce Motion | User enables Reduce Motion | No disruptive animations during OTP entry | High |
| PRIV1 | Privacy | Permission Prompt | App does not request unnecessary permissions (e.g., Contacts) before OTP screen | Only required permissions (Notifications) are asked | High |
How to Use the Matrix
- Automation Priority: Items marked “High” feasibility are prime candidates for XCUITest or similar UI automation. “Medium” items may need network simulation or device‑specific features (AutoFill). “Low” items usually require backend stubs or security‑focused tooling (e.g., OWASP ZAP, Frida).
- Manual Exploration: Use the matrix as a checklist for exploratory testing. Tick off each row after you verify the behavior on a real device or simulator.
- Regression Guard: When you add a new feature that touches the OTP flow (e.g., adding email‑based OTP), duplicate the matrix and adjust expectations accordingly.
Manual Testing Approach Step‑by‑Step
Even with strong automation, manual verification catches nuances that scripts overlook—especially around timing, interruptions, and human perception. Follow this procedure on a clean iOS device (or simulator with appropriate capabilities).
1. Environment Preparation
- Device: Use a physical iPhone running iOS 16+ for AutoFill and push‑notification realism. Keep a second device handy to send SMS via a test carrier or a service like Twilio.
- Network: Enable the Network Link Conditioner (on macOS) or use a tool like Charles Proxy to simulate 3G, LTE, and offline states.
- Console: Connect the device to Xcode, open the Debug console, and set a breakpoint on
OSLogwithsubsystem: "com.yourapp.otp"to ensure no OTP leaks. - Accessibility: Turn on VoiceOver, Bold Text, Larger Text, and Reduce Motion in Settings → Accessibility to validate each scenario.
2. Happy Path Execution
- Trigger the OTP flow (e.g., tap “Sign Up” → enter phone number → request code).
- Observe the system notification or QuickType bar that shows the OTP.
- Tap the suggestion to autofill; verify the field populates exactly six digits.
- Tap Verify; confirm navigation to the next screen and that a success toast or analytics event fires.
- Log the request/response pair using Charles to ensure the OTP was sent to the backend and validated.
3. Error Path Execution
- Wrong OTP: Manually type**: Verify button stays enabled.
- Expired OTP: Wait until the server‑side TTL elapses (configure your mock to return 410 after 2 minutes), then enter the previously valid code; confirm “Code expired” appears.
- Network loss: Activate Airplane Mode after you have entered the OTP but before tapping Verify; the UI should display a connectivity error and not crash.
- Server error: Point the endpoint to a stub that returns HTTP 500; verify a generic error screen with a retry button appears.
4. Edge Case Execution
- Rapid Resend: Tap the Resend button three times within a second; ensure only one network request is logged and a cooldown timer (e.g., “Resend in 30 s”) appears.
- App Background: After OTP arrives, press Home, open Messages to read the code, wait 20 s, then restore the app; the OTP field should still show the entered digits (if any) and the Verify button remain enabled.
- Interruption Alert: While the OTP field is first responder, trigger a system permission request (e.g., Location When In Use) via Settings; after dismissing the alert, confirm the OTP field regains focus and the entered text is intact.
- VoiceOver: Enable VoiceOver, navigate to the OTP field using swipe gestures; verify the hint reads “Enter the 6‑digit code you received”. Use the keyboard to enter digits; each digit should be spoken as typed.
- RTL: Change device language to Arabic, verify that the OTP field aligns to the right and the cursor moves left when typing.
5. Accessibility & Localization Checks
- Run the app with Dynamic Type set to the largest size; ensure no clipped labels and that the OTP field remains tappable.
- Switch to a right‑to‑left language; confirm that the “Resend OTP” button mirrors correctly.
- Take a screenshot and run it through a color‑contrast analyzer (WCAG AA minimum 4.5:1 for normal text).
6. Security & Privacy Spot Checks
- Log Scrubbing: After a successful verification, search the device console for the OTP string (
grep -r "123456"in the device logs viaConsole.app). It should not appear. - Rate Limit Simulation: Using a backend stub, send 12 rapid wrong OTP requests; confirm the server responds with 429 Too Many Requests and the UI shows a cooldown message.
- Replay Attempt: Capture a valid OTP from the network, wait until the TTL expires, then resend the same code via a manual POST; verify the server rejects it.
7. Documentation
For each test case, record:
- Device model & iOS version
- Network condition (if applicable)
- Steps taken
- Observed result
- Pass/Fail
- Any screenshots or console logs
Store these records in a test‑management tool (e.g., TestRail) or a simple spreadsheet; they become the baseline for regression.
Automated Testing with XCUITest
Apple’s UI testing framework offers the most reliable way to script OTP verification on iOS because it runs inside the same process as the app, granting direct access to UI elements and the ability to mock network responses.
Project Setup
- Add a UI Testing target if you don’t already have one (File → New → Target → UI Testing Bundle).
- Ensure your app’s OTP text field has an accessibility identifier, e.g.,
otpTextField. - Add a
UITextContentTypeof.oneTimeCodeto enable AutoFill on iOS 12+.
// In your view controller or SwiftUI view
otpTextField.accessibilityIdentifier = "otpTextField"
otpTextField.textContentType = .oneTimeCode
Mocking Network Layer
Use a protocol‑based network client that can be swapped for a stub in UI tests.
protocol OTPService {
func requestCode(for phone: String, completion: @escaping (Result<Void, Error>) -> Void)
func verifyCode(_ code: String, completion: @escaping (Result<Bool, Error>) -> Void)
}
class OTPServiceStub: OTPService {
var shouldSucceed = true
var delay: TimeInterval = 0.5
func requestCode(for phone: String, completion: @escaping (Result<Void, Error>) -> Void) {
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
if self.shouldSucceed {
completion(.success(()))
} else {
completion(.failure(NSError(domain: "Test", code: -1, userInfo: nil)))
}
}
}
func verifyCode(_ code: String, completion: @escaping (Result<Bool, Error>) -> Void) {
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
if self.shouldSucceed && code == "123456" {
completion(.success(true))
} else {
completion(.failure(NSError(domain: "Test", code: -1, userInfo: nil)))
}
}
}
}
Inject the stub via an environment variable or a ProcessInfo flag:
if ProcessInfo.processInfo.environment["UI_TEST"] == "1" {
ServiceLocator.shared.otpService = OTPServiceStub()
}
Writing the Test
import XCTest
final class OTPVerificationUITests: XCTestCase {
var app: XCUIApplication!
override func setUp() {
continueAfterFailure = false
app = XCUIApplication()
app.launchEnvironment["UI_TEST"] = "1"
app.launch()
}
func testHappyPathWithAutoFill() {
// 1. Trigger OTP request
let phoneField = app.textFields["phoneNumber"]
XCTAssertTrue(phoneField.exists)
phoneField.tap()
phoneField.typeText("+15551234567")
app.buttons["Send Code"].tap()
// 2. Wait for the system AutoFill suggestion
let otpField = app.textFields["otpTextField"]
let exists = NSPredicate(format: "exists == true")
expectation(for: exists, evaluatedWith: otpField, handler: nil)
waitForExpectations(timeout: 10, handler: nil)
// 3. Simulate tapping the QuickType bar (requires iOS 12+ device)
// XCUITest cannot directly tap the suggestion bar; instead we set the value.
otpField.tap()
otpField.typeText("123456") // In real device, AutoFill would populate this.
// 4. Tap Verify and assert navigation
app.buttons["Verify"].tap()
let welcome = app.staticTexts["Welcome"]
XCTAssertTrue(waitForElementToAppear(welcome, timeout: 5))
}
func testWrongOTPShowsError() {
// Setup same as above until OTP field
// ... (omitted for brevity)
otpField.typeText("654321")
app.buttons["Verify"].tap()
let error = app.staticTexts["Invalid code"]
XCTAssertTrue(waitForElementToAppear(error, timeout: 5))
}
// Helper
private func waitForElementToAppear(_ element: XCUIElement, timeout: TimeInterval) -> Bool {
let exists = NSPredicate(format: "exists == true")
return expectation(for: exists, evaluatedWith: element, handler: nil)
.waitForExpectations(timeout: timeout, handler: nil) == nil
}
}
Key Points
launchEnvironment["UI_TEST"] = "1"injects the stub.- AutoFill cannot be triggered directly in the simulator; on a real device you can rely on the system QuickType bar by setting
textContentType = .oneTimeCode. In CI, manually typing the code is acceptable because the field’s logic is the same. - Use
NSPredicate‑based expectations to wait for asynchronous UI updates (network stub delays, AutoFill appearance). - Always reset the stub’s state (
shouldSucceed,delay) insetUpor via launch arguments if you need to simulate failures.
Simulating Network Conditions
Combine Network Link Conditioner (on macOS) with XCUIDevice orientation changes, or use a third‑party library like Mockey to stub HTTP responses directly in the UI test target. For carrier‑specific filtering, you can use a tool like Charles Proxy to rewrite SMS‑gateway responses (though true carrier behavior requires a physical SIM and a test number).
Parallel Execution
Mark your test class with @available(iOS 13, *) and enable parallel testing in the scheme options to cut down CI time. Ensure that your network stub is thread‑safe or uses a fresh instance per test.
Alternative iOS Automation Tools
While XCUITest is the default, other frameworks can be useful depending on your stack.
| Tool | Language | Strengths for OTP Testing | Limitations |
|---|---|---|---|
| EarlGrey | Objective‑C / Swift | Synchronized actions, built‑in waiting for animations, good for complex gestures | Less community support, requires Bridging Header for Swift projects |
| Detox | JavaScript (via Detox CLI) | Gray‑box approach, can sync with network mocks, works well with React Native | Requires Android/iOS separate setup, slower startup |
| Appium | Any (via WebDriverJSONWireProtocol) | Cross‑platform, can test real device farms, supports SMS gateways via plugins | Higher overhead, less reliable for timing‑sensitive AutoFill |
| Firebase Test Lab | Any (via gcloud) | Runs on real Google‑owned devices, can simulate different locales and hardware | Cost per minute, limited control over custom network stubs |
Example: Detox OTP Test (JavaScript)
describe('OTP Verification', () => {
beforeEach(async () => {
await device.launchApp({ newInstance: true, permissions: { notifications: 'YES' } });
});
it('should verify correct OTP', async () => {
// Trigger OTP request
await element(by.id('phoneNumber')).typeText('+15551234567');
await element(by.id('sendCode')).tap();
// Wait for OTP field to be ready
await expect(element(by.id('otpTextField'))).toBeVisible();
await element(by.id('otpTextField')).typeText('123456');
// Submit
await element(by.id('verify')).tap();
// Expect success screen
await expect(element(by.id('welcomeScreen'))).toBeVisible();
});
});
Detox’s synchronization eliminates most sleep calls, making the test resilient to animation delays.
Accessibility and Localization Considerations
Accessibility bugs often hide in plain sight because they do not affect the majority of users but can block a significant segment. Localization issues surface when the app is released in new markets.
Accessibility Checklist for OTP Screens
- Label & Hint: Every OTP text field must have an
accessibilityLabel(e.g., “Enter verification code”) and anaccessibilityHint(e.g., “Six‑digit code you received via SMS”). VoiceOver should read both when the field gains focus. - Dynamic Type: Verify that the field’s font scales with the user’s preferred text size. Use
UIFontMetricsor SwiftUI’s.font(.body)to honorUIContentSizeCategory. - Reduce Motion: If you animate the OTP field (e.g., a shake on error), respect
UIAccessibility.isReduceMotionEnabledby either disabling the animation or substituting a non‑motion indicator (color change or vibration). - Touch Target Size: The OTP field and any associated buttons (Resend, Verify) must meet the 44 × 44 pt minimum. Use the Accessibility Inspector to confirm.
- Color Contrast: Ensure placeholder text and error messages have at least 4.5 : 1 contrast against the background. Run the axe core or Contrast Checker on screenshots.
Localization Test Matrix (Subset)
| Locale | Layout Direction | OTP Prompt Translation | Button Labels | Date/Time Format (if shown) |
|---|---|---|---|---|
| en‑US | LTR | “Enter the 6‑digit code you received” | Send Code / Verify | MM/dd/yyyy |
| ar‑SA | RTL | “أدخل رمز التأكيد المكون من 6 أرقام الذي تلقيته” | إرسال الرمز / تحقق | dd/MM/yyyy |
| ja‑JP | LTR | “受信した6桁のコードを入力してください” | コードを送信 / 確認 | yyyy/MM/dd |
| de‑DE | LTR | “Geben Sie den 6‑stelligen Code ein, den Sie erhalten haben” | Code senden / Bestätigen | dd.MM.yyyy |
Run your UI tests with each locale by setting ProcessInfo.processInfo.environment["AppleLocale"] in the test’s setUp method, then assert that all align correctly (if the OTP field does not clip screenshots and run them through an automated localization lint tool (e.g., SwiftGen or SwiftLint with a custom rule) to catch missing .strings entries.
Security and Privacy Testing for OTP Flows
Security testing goes beyond functional correctness; it validates that the OTP cannot be abused, leaked, or bypassed.
Threat Model Overview
| Threat | Vector | Mitigation |
|---|---|---|
| Brute‑Force | Repeated wrong OTP submissions | Server‑side rate lockout, exponential backoff, UI‑level cooldown |
| Replay Attack | Capturing a valid OTP and re‑using later | Server validates timestamp / nonce, short TTL (≤ 2 min) |
| SIM‑Swap | Attacker convinces carrier to port number | Optional: device‑binding, push‑notification‑based OTP as fallback |
| Log Leakage | OTP appears in console, crash logs, or analytics | Never log OTP; use OSLog with .privacy = .private or omit entirely |
| Side‑Channel | Timing differences between valid/invalid codes | Constant‑time comparison on backend; client should not reveal validity via UI timing |
| Man‑in‑the‑Middle | Intercepting OTP via rogue Wi‑Fi or malicious profile | Enforce HTTPS with certificate pinning; detect compromised profiles via NEHotspotHelper (if applicable) |
Practical Security Tests
- Rate Limit Validation
- Stub the backend to return
429 Too Many Requestsafter five failed attempts. - In the UI test, loop sending wrong OTPs and assert that a cooldown timer appears and further attempts are blocked until the timer expires.
- Replay Attempt
- Capture a valid OTP from the network stub (you can log it in the test).
- Wait until the TTL elapses (configure stub to treat any code older than 90 seconds as invalid).
- Resend the same OTP and verify the server returns an error and the UI shows “Code expired”.
- Log Scrubbing
- Run the app on a device, trigger OTP verification, then pull the device console via
Console.apporxcrun simctl spawn booted log show --predicate 'process == "YourApp"' --last 5m. - Grep for the OTP string; ensure zero matches.
- Certificate Pinning
- Use a tool like TrustKit to enforce pinning.
- In a test environment, install a self‑signed certificate on the device and confirm the request fails with
NSURLErrorServerCertificateUntrusted. - Verify that the app shows a user‑friendly error (e.g., “Unable to connect securely”) rather than crashing.
- Push‑Notification OTP Fallback
- If your app supports push‑based OTP (via APNs), disable SMS in the test backend and ensure the push payload contains the code and that the UI correctly reads it from the notification payload (using
UNUserNotificationCenterdelegate).
All of these tests can be automated with a combination of XCUITest (UI assertions) and a backend stub (e.g., Vapor, Express, or MockServer) that you launch in your CI pipeline.
Autonomous Persona‑Driven Exploration
Scripted tests excel at verifying known paths, but they rarely stray from the happy‑path assumptions encoded in the test code. Autonomous exploration tools—like SUSATest—drive the app with a variety of simulated user personas, each embodying distinct behavior patterns, tolerances, and goals. This approach surfaces bugs that only manifest under atypical interaction styles, such as an impatient user repeatedly tapping, an elderly user struggling with small touch targets, or a curious user digging into hidden menus.
How Persona‑Driven Testing Works
- Persona Profiles – Each persona defines:
- Interaction speed (e.g., *Impatient*: 0.2 s between taps; *Elderly*: 1.0 s + longer press duration)
- Error tolerance (e.g., *Novice*: more likely to mis‑type, *Adversarial*: attempts out‑of‑order actions)
- Input strategy (e.g., *Power user*: uses paste and AutoFill; *Accessibility*: relies on VoiceOver)
- Exploration Engine – The tool crawls the UI state graph, applying the persona’s policy to decide the next action (tap, swipe, type, voice command). It remembers visited screens and dead ends, expanding coverage over successive runs.
- Oracle – Built‑in checks detect crashes, ANRs, accessibility violations, security red flags (e.g., logging of sensitive data), and UX friction (e.g., repeated failed attempts without feedback).
Applying Persona Exploration to OTP Verification
When SUSATest (or a similar autonomous agent) targets an OTP screen, it can generate test variations such as:
- Curious Persona: After receiving the OTP, the persona navigates away to the settings menu, returns after a delay, and attempts to verify with a stale code—testing server‑side TTL enforcement and UI state preservation.
- Impatient Persona: Rapidly taps the Resend button ten times in two seconds, exposing missing debouncing or excessive network calls.
- Elderly Persona: Uses a simulated tremor (random offset taps) to see if the OTP field’s hit area is too small, revealing touch‑target violations.
- Adversarial Persona: Enters non‑numeric strings, emojis, or extremely long paste content to test input sanitization and buffer overflow guards.
- Accessibility Persona: Relies exclusively on VoiceOver gestures, ensuring that hints are announced and that the rotor can adjust values if the field were implemented as a custom picker.
- Power User: Copies the OTP from a notification, pastes it, then immediately taps Verify without looking at the screen—validating that AutoFill works and that the Verify button enables instantly upon valid input.
These behaviors are difficult to anticipate in a manual test matrix because they combine timing, interaction quirks, and cognitive load. Autonomous exploration catches them early, often before they reach production, and the resulting regression scripts (Appium for Android, Playwright for Web, or XCUITest for iOS) can be added to your suite for continuous verification.
Integrating SUSATest into Your Workflow
- CLI Installation:
pip install susatest-agent - Execution:
susatest run --app MyApp.ipa --personas impatient,elderly,accessible --duration 10m - Output: The agent produces a JSON report listing discovered issues, each with severity, steps to reproduce, and a generated XCUITest snippet.
- Feedback Loop: Import the generated XCUITest tests into your CI pipeline; they act as a living regression suite that evolves as the agent learns new dead ends.
While SUSATest provides valuable complementary coverage, it should augment—not replace—your deliberate test matrix and automated checks. Use its findings to prioritize additional unit or UI tests, refine accessibility labels, or tighten backend validation logic.
Checklist for OTP Verification Testing
Copy this list into your project’s wiki or README. Tick each item as you complete it.
[ ] Happy Path
[ ] Correct OTP entry leads to success
[ ] AutoFill populates field correctly (iOS 12+)
[ ] Pasting OTP works
[ ] Resend OTP triggers single network request with cooldown timer
[ ] Error Paths
[ ] Wrong OTP shows inline error, field stays enabled
[ ] Expired OTP shows “Code expired” and offers resend
[ ] Malformed input (letters, too short/long) is rejected instantly
[ ] Network loss after OTP entry shows connectivity error, no crash
[ ] Server 500 returns generic error with retry option
[ ] Edge Cases
[ ] Rapid Resend (≤ 2 s) does not spam backend
[ ] App background/restore preserves entered OTP
[ ] System interruption (alert, call) does not lose focus or data
[ ] VoiceOver reads label, hint, and announces each typed digit
[ ] RTL layout mirrors correctly and input direction respects language
[ ] Accessibility
[ ] Dynamic Type scales all text and touch targets
[ ] Reduce Motion disables non‑essential animations
[ ] Color contrast meets WCAG AA for normal and large text
[ ] TalkBack/VoiceOver navigation reaches OTP field without getting stuck
[ ] Security
[ ] Rate limiting blocks after N failed attempts (backend & UI)
[ ] Replay attempts after TTL are rejected
[ ] OTP never appears in console or crash logs (grep verification)
[ ] Certificate pinning blocks MITM attempts with self‑signed cert
[ ] Push‑notification OTP (if supported) is parsed correctly
[ ] Privacy
[ ] No unnecessary permission requests before OTP screen
[ ] Analytics events do not include OTP value
[ ] Localization
[ ] All strings present for supported locales
[ ] Layout does not truncate in longest language (typically German)
[ ] Date/Time formats respect locale when displayed
[ ] Regression
[ ] Generated XCUITest (or Detox/EarlGrey) tests cover at least 80 % of matrix rows
[ ] CI runs the OTP test suite on every PR and nightly on device farm
Closing Takeaways
Testing OTP verification on iOS is more than checking that a six‑digit field accepts numbers. It demands a layered strategy:
- Define the contract – What constitutes a valid OTP, what are the timing constraints, and how does the system communicate success or failure?
- Build a deterministic matrix – Cover happy path, error paths, edge cases, accessibility, localization, and security. Use the table in this guide as a starter; extend it with product‑specific flows (e.g., email‑based OTP, authenticator‑app codes).
- Automate the repeatable – Write XCUITest (or Detox/EarlGrey) tests that drive the UI, stub network responses, and assert on both visible state and hidden logs. Leverage
textContentType = .oneTimeCodefor AutoFill compatibility. - Validate the non‑obvious – Run manual exploratory sessions that simulate
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