How to Test Forgot Password on iOS (Complete Guide)
How to Test Forgot Password on iOS (Complete Guide): Why It Matters
How to Test Forgot Password on iOS (Complete Guide): Why It Matters
Testing the forgot‑password flow is a high‑risk area because it directly touches user authentication, account recovery, and data privacy. A broken reset mechanism can lead to account takeover, credential leakage, or denial‑of‑service for legitimate users. In iOS apps the flow often involves a combination of UI elements (text fields, buttons, links), deep‑link handling, backend API calls, and sometimes biometric fallback. When any of these pieces misbehave, users experience friction, support costs rise, and the app’s reputation suffers.
A systematic approach—starting with a clear test matrix, moving through manual verification, then layering automation and autonomous exploration—helps catch the subtle bugs that only appear under specific conditions (e.g., VoiceOver enabled, intermittent network, or when a power user pastes a very long token). This guide walks you through each step, provides concrete iOS‑specific examples, and shows how a persona‑driven autonomous tool like SUSA can surface issues that scripted tests never think to try.
---
How to Test Forgot Password on iOS (Complete Guide): Building the Test Matrix
A solid test matrix separates the happy path from error conditions, edge cases, accessibility checks, and security/privacy concerns. Below is a comprehensive matrix you can copy into a test‑plan spreadsheet. Each row includes a test ID, description, expected result, and notes on iOS‑specific considerations.
| Test ID | Category | Description | Expected Result | iOS‑Specific Notes |
|---|---|---|---|---|
| FPIOS‑01 | Happy Path | User taps “Forgot Password?” on login screen, enters valid registered email, receives reset link via email, clicks link, sets new password, logs in with new credentials. | Reset succeeds, new password works, old password invalidated. | Verify that the email composition UI (MFMailComposeViewController or universal link) presents correctly; check that the app returns to foreground after link tap via Universal Links. |
| FPIOS‑02 | Happy Path | Same as FPIOS‑01 but user initiates flow from a Settings screen rather than login. | Same outcome as FPIOS‑01. | Ensure deep link handling works from any entry point (e.g., myapp://reset?token=…). |
| FPIOS‑03 | Error Path | User enters an email that is not registered in the backend. | App shows a generic “If the email exists, you’ll receive instructions” message (no enumeration). | Backend must return same HTTP status (200) and UI must not reveal existence. |
| FPIOS‑04 | Error Path | User leaves email field blank and taps Submit. | Inline validation shows “Email is required”. | Use UITextFieldDelegate shouldChangeCharactersIn to enforce real‑time check; ensure error label is accessible (isAccessibilityElement = true). |
| FPIOS‑05 | Error Path | User enters a malformed email (e.g., “user@”). | Inline validation shows “Please enter a valid email address”. | Regex should allow iOS‑accepted characters; test with Unicode domains. |
| FPIOS‑06 | Error Path | Network request times out (simulate with Network Link Conditioner set to 100% loss). | App displays a retryable network error, does not crash, and retains entered email. | Use URLSession with timeout; verify that UI does not dismiss the screen automatically. |
| FPIOS‑07 | Error Path | Backend returns 429 Too Many Requests (rate limit). | App shows a friendly “Too many attempts, try again later” and disables resend button for a cool‑down period. | Ensure the UI respects the Retry-After header if present. |
| FPIOS‑08 | Edge Case | User pastes a 500‑character string into the email field. | Field accepts paste but validation rejects as invalid email; UI does not hang. | Check that UITextField does not exceed its intrinsic content size causing layout glitches. |
| FPIOS‑09 | Edge Case | User triggers the flow while VoiceOver is ON and navigates using swipe gestures. | All controls are announced correctly; focus moves to error messages when they appear. | Test with UIAccessibility.isVoiceOverRunning; ensure accessibilityHint is set on the submit button. |
| FPIOS‑10 | Edge Case | User has Dynamic Type set to largest accessibility size. | Layout scales, no clipping, buttons remain tappable. | Use UIFontMetrics for scaling; verify with Xcode’s Environment Overrides. |
| FPIOS‑11 | Edge Case | User enables Reduce Motion; the app uses a modal transition for the reset screen. | Transition respects reduced motion (fade or slide disabled). | Check UIViewPropertyAnimator or UIView.animate with UIView.AnimationOptions.allowUserInteraction. |
| FPIOS‑12 | Security/Privacy | Reset token appears in URL bar of Safari when universal link opens. | Token is short‑lived (≤15 min) and transmitted over HTTPS only; no logging of token in plain text. | Use OSLog with .private to avoid token leakage; verify with network sniffing (Charles). |
| FPIOS‑13 | Security/Privacy | App allows unlimited reset requests from the same device/IP in a short window. | Server enforces rate limiting (e.g., 5 requests per 15 min) and app shows appropriate feedback. | Test with a script that fires rapid requests; ensure no account enumeration via timing differences. |
| FPIOS‑14 | Security/Privacy | After password reset, old sessions remain valid. | All existing tokens are invalidated; user must re‑login on all devices. | Verify backend revokes refresh tokens; test by logging in on a second device before reset. |
| FPIOS‑15 | Security/Privacy | Reset screen does not mask password entry (shows characters). | New password fields are isSecureTextEntry = true. | Confirm with Accessibility Inspector that the field is marked as secure. |
*Use this matrix as a baseline; add project‑specific variants (e.g., social‑login linked accounts, corporate SSO, or OTP‑based reset).*
---
How to Test Forgot Password on iOS (Complete Guide): Manual Step‑by‑Step Testing Procedure
Manual testing remains valuable for exploratory checks, especially when validating accessibility and subtle UI glitches. Follow this procedure on a physical device running the latest iOS version supported by your app (simulator can miss certain hardware‑related behaviors like pasteboard restrictions).
- Prepare the Environment
- Install the latest build via TestFlight or Xcode.
- Clear any existing user data (Settings → General → iPhone Storage → App → Offload App, then reinstall).
- Enable Network Link Conditioner (Developer → Network) to simulate various profiles (Good 3G, LTE, 100% Loss).
- Turn on Accessibility Inspector (Settings → Accessibility → Accessibility Shortcut → Inspector) for real‑time UI checks.
- Happy Path Verification
- From the login screen, tap Forgot Password?
- Enter a known‑good email address (use a test mailbox you control).
- Tap Submit.
- Observe a success toast or placeholder message (“Check your inbox”).
- Switch to the mail app, locate the reset email, and tap the universal link.
- The app should open directly to the reset‑password screen (deep link handled via
application(_:continue:restorationHandler:)). - Enter a new password that meets policy, confirm, and tap Reset.
- Return to login and sign in with the new credentials; ensure the old password fails.
- Error Path Checks
- Repeat the flow with an unregistered email; verify the generic message appears and no network call reveals existence (check with Charles or Wireshark).
- Submit with empty field; ensure inline validation appears immediately without a network round‑trip.
- Submit with malformed email; confirm regex‑based error.
- Simulate a timeout (set Network Link Conditioner to 100% loss) and tap Submit; ensure the app shows a retryable error and does not crash.
- Edge‑Case Exploration
- Paste a very long string into the email field (use the pasteboard). Observe that the UI does not freeze and validation runs promptly.
- Enable VoiceOver, navigate to the email field, and double‑tap to activate; confirm the field is announced as “Email text field, is editing”.
- Increase Dynamic Type to the largest setting; verify that all labels and buttons resize without overlapping.
- Turn on Reduce Motion; trigger the reset screen and ensure the transition is a simple fade.
- Test with an external keyboard attached; ensure that Return key submits the form and that Tab moves focus correctly.
- Security & Privacy Spot‑Checks
- While the reset link is open in Safari, enable the Develop menu and inspect the URL; confirm the token is present only in the query string and that the request to your backend uses
https://. - After reset, log in on a second device with the old credentials; the login should fail, indicating server‑side token invalidation.
- Attempt to trigger the reset endpoint 20 times in rapid succession via
curl(see automation section) and verify the backend returns 429 after the allowed threshold.
- Document Findings
- For each test case, record: build version, device model, iOS version, network condition, accessibility settings, and outcome (PASS/FAIL with screenshots or logs).
- Attach any console logs (
Device > Consolein Xcode) or network traces that helped diagnose failures.
---
How to Test Forgot Password on iOS (Complete Guide): Automated Testing with XCTest / XCUITest
XCUITest is Apple’s UI testing framework and integrates naturally with Xcode CI pipelines. It allows you to drive the app exactly the same interactions a user performs while asserting on UI elements and network mocks. Below is a practical setup and sample tests.
Setting Up the Test Target
- In Xcode, select File → New → Target → UI Testing Bundle.
- Ensure the bundle’s
Info.plistcontainsUITestingkey set toYES. - Add a helper class to manage app state:
import XCTest
final class ForgotPasswordUITestHelper {
let app: XCUIApplication
init() {
app = XCUIApplication()
app.launchArguments.append("-UI_TESTING")
app.launchEnvironment["UI_TESTING"] = "1"
}
func launch() {
app.launch()
}
func term() {
app.terminate()
}
}
- Create a mock networking layer (using
URLProtocol) that returns predefined JSON for the reset API. This lets you test success, validation errors, and rate‑limit responses without hitting a real server.
Sample XCTest Code for Happy Path
import XCTest
final class ForgotPasswordHappyPathTests: XCTestCase {
var helper: ForgotPasswordUITestHelper!
override func setUp() {
super.setUp()
helper = ForgotPasswordUITestHelper()
helper.launch()
continueAfterFailure = false
}
override func tearDown() {
helper.term()
super.tearDown()
}
func testResetPasswordSuccess() {
let emailField = helper.app.textFields["Email"]
XCTAssertTrue(emailField.waitForExistence(timeout: 5))
emailField.tap()
emailField.typeText("user@example.com")
let submitBtn = helper.app.buttons["Submit"]
XCTAssertTrue(submitBtn.isEnabled)
submitBtn.tap()
// Expect a success alert
let successAlert = helper.app.staticTexts["Check your inbox for the reset link"]
XCTAssertTrue(successAlert.waitForExistence(timeout: 5))
// Simulate tapping the universal link (we deep-link directly)
helper.app.launchEnvironment["RESET_TOKEN"] = "valid-token-123"
helper.app.terminate()
helper.launch()
let newPwd = helper.app.secureTextFields["New Password"]
let confirmPwd = helper.app.secureTextFields["Confirm Password"]
XCTAssertTrue(newPwd.waitForExistence(timeout: 5))
newPwd.tap()
newPwd.typeText("NewP@ssw0rd!")
confirmPwd.tap()
confirmPwd.typeText("NewP@ssw0rd!")
helper.app.buttons["Reset Password"].tap()
// After reset, we should be back at login screen
let loginBtn = helper.app.buttons["Log In"]
XCTAssertTrue(loginBtn.waitForExistence(timeout: 5))
// Try login with new password
let loginEmail = helper.app.textFields["Email"]
loginEmail.tap()
loginEmail.typeText("user@example.com")
let loginPwd = helper.app.secureTextFields["Password"]
loginPwd.tap()
loginPwd.typeText("NewP@ssw0rd!")
helper.app.buttons["Log In"].tap()
// Verify we reach the home screen (customize identifier)
let homeTab = helper.app.tabBars.buttons["Home"]
XCTAssertTrue(homeTab.waitForExistence(timeout: 5))
}
}
Sample XCTest Code for Error Paths
func testEmptyEmailShowsValidation() {
helper.launch()
let submit = helper.app.buttons["Submit"]
submit.tap() // try to submit with empty field
let error = helper.app.staticTexts["Email is required"]
XCTAssertTrue(error.waitForExistence(timeout: 3))
}
func testRateLimitShowsRetry() {
// Configure mock to return 429 on the third call
helper.launch()
let email = helper.app.textFields["Email"]
email.tap()
email.typeText("test@example.com")
// First two attempts succeed (mock returns 200)
for _ in 0..<2 {
helper.app.buttons["Submit"].tap()
XCTAssertTrue(helper.app.staticTexts["Check your inbox"].waitForExistence(timeout: 3))
// reset mock for next call
}
// Third attempt – should hit rate limit
helper.app.buttons["Submit"].tap()
let limitMsg = helper.app.staticTexts["Too many attempts, try again later"]
XCTAssertTrue(limitMsg.waitForExistence(timeout: 3))
// Ensure submit button is disabled
XCTAssertFalse(helper.app.buttons["Submit"].isEnabled)
}
Handling Asynchronous Network Calls
- Use
XCTestExpectationwith a notification from your networking layer (e.g., a customNotification.Name.resetResponseReceived). - Alternatively, rely on UI changes that only appear after the network call finishes (e.g., an activity indicator disappearing).
- Keep timeouts generous (10‑15 seconds) to accommodate CI variability, but fail fast if the UI does not update.
Running Tests in CI
Add a step to your fastlane or GitHub Actions workflow:
xcodebuild test \
-workspace MyApp.xcworkspace \
-Scheme MyAppUITests \
-destination 'platform=iOS Simulator,name=iPhone 15,OS=latest' \
-only-testing:ForgotPasswordHappyPathTests/testResetPasswordSuccess \
-only-testing:ForgotPasswordErrorPathTests/testEmptyEmailShowsValidation \
-only-testing:ForgotPasswordErrorPathTests/testRateLimitShowsRetry \
| xcpretty
---
How to Test Forgot Password on iOS (Complete Guide): Automated Testing with Appium (iOS)
Appium enables cross‑platform UI automation using the WebDriver protocol. It’s handy when you want to run the same script against real devices, simulators, or even cloud farms (Sauce Labs, BrowserStack). Below is a concise guide to setting up Appium for iOS forgot‑password testing, plus a parameterized script that can emulate different user personas.
Installing Appium and Dependencies
# Install Node.js (if not present)
brew install node
# Install Appium server globally
npm install -g appium
# Install the Apple‑specific driver
appium driver install xcuitest
# Install client libraries (choose your language)
# Python example:
pip install Appium-Python-Client
# JavaScript example:
npm install webdriverio appium
Ensure you have Xcode command‑line tools and the WebDriverAgent project built:
# After installing the driver, bootstrap WebDriverAgent
appium driver install xcuitest --force
# Then start Appium server:
appium
Sample Appium Script (Python)
import time
import unittest
from appium import webdriver
from appium.options.ios import XCUITestOptions
class ForgotPasswordAppiumTest(unittest.TestCase):
def setUp(self):
options = XCUITestOptions()
options.platform_name = "iOS"
options.platform_version = "17.2"
options.device_name = "iPhone 15"
options.app = "/path/to/MyApp.ipa" # or bundle ID for simulator
options.automation_name = "XCUITest"
# Disable animation for faster runs
options.set_capability("disableAnimations", True)
self.driver = webdriver.Remote("http://127.0.0.1:4723", options=options)
def tearDown(self):
self.driver.quit()
def test_reset_password_with_persona(self, persona="novice"):
# Persona‑based data – adjust typing speed, error tolerance, etc.
persona_data = {
"novice": {"email": "novice@example.com", "pwd": "Novice123!", "type_delay": 0.3},
"power": {"email": "poweruser@example.com", "pwd": "P0w3r!$", "type_delay": 0.05},
"impatient": {"email": "imp@example.com", "pwd": "Imp!23", "type_delay": 0.01},
}
data = persona_data.get(persona, persona_data["novice"])
driver = self.driver
# Wait for login screen
email_field = driver.find_element(by="accessibility id", value="Email")
email_field.click()
email_field.send_keys(data["email"])
# Simulate a typo for novice persona
if persona == "novice":
email_field.send_keys("m") # accidental extra char
time.sleep(0.2)
email_field.clear()
email_field.send_keys(data["email"])
submit = driver.find_element(by="accessibility id", value="Submit")
submit.click()
# Wait for success toast
toast = driver.find_element(by="xpath", value="//XCUIElementTypeStaticText[@name='Check your inbox']")
self.assertTrue(toast.is_displayed())
# Deep‑link simulation – set env var to trigger reset screen directly
driver.terminate_app("com.example.myapp")
driver.activate_app("com.example.myapp")
# Assuming the app reads launch env var RESET_TOKEN
driver.set_launch_arguments(["-RESET_TOKEN", "test-token-abc"])
# Fill new password
new_pwd = driver.find_element(by="accessibility id", value="New Password")
new_pwd.click()
new_pwd.send_keys(data["pwd"])
confirm_pwd = driver.find_element(by="accessibility id", value="Confirm Password")
confirm_pwd.click()
confirm_pwd.send_keys(data["pwd"])
driver.find_element(by="accessibility id", value="Reset Password").click()
# Verify back to login
login_btn = driver.find_element(by="accessibility id", value="Log In")
self.assertTrue(login_btn.is_displayed())
# Login with new credentials
email_field.clear()
email_field.send_keys(data["email"])
pwd_field = driver.find_element(by="accessibility id", value="Password")
pwd_field.send_keys(data["pwd"])
driver.find_element(by="accessibility id", value="Log In").click()
# Home screen indicator
home_tab = driver.find_element(by="accessibility id", value="Home")
self.assertTrue(home_tab.is_displayed())
if __name__ == "__main__":
suite = unittest.TestSuite()
for p in ["novice", "power", "impatient"]:
suite.addTest(ForgotPasswordAppiumTest("test_reset_password_with_persona", p))
runner = unittest.TextTestRunner(verbosity=2)
runner.run(suite)
Explanation of persona handling:
- The script modifies typing delay and introduces a deliberate typo for the “novice” persona to mimic a user who might hit the wrong key and then correct it.
- The “impatient” persona types quickly and may tap the submit button before the field validates; you can add a check that the app still shows inline validation rather than proceeding.
- The “power” persona may paste a strong password from a password manager; you can simulate that with
set_clipboardandpaste.
Running Against Real Devices
Connect a device via USB, trust the computer, and specify its UDID in the capabilities:
options.set_capability("udid", "00008020-001A672C1234001E")
Make sure the device is developer‑enabled and that the app is installed (via ios-deploy or Xcode).
---
How to Test Forgot Password on iOS (Complete Guide): Autonomous, Persona‑Driven Exploration with SUSA
SUSA (SUSATest) is an autonomous QA agent that explores an iOS app without pre‑written scripts. It builds a behavioral model of the app, applies a set of user‑persona profiles, and attempts to achieve goals (like completing a reset flow) while logging any anomalies. Because SUSA does not rely on hard‑coded selectors, it can discover flows that are hidden behind conditional UI, dynamic deep links, or accessibility‑only pathways.
How SUSA Discovers Forgot Password Flows
- Entry Point Detection – On launch, SUSA scans the view hierarchy for elements with accessibility labels containing words like “forgot”, “reset”, “recover”, or “password”. It also inspects
UITextFieldplaceholders andUIButtontitles. - Goal Formulation – Once a candidate is found, SUSA creates a goal: “Reach a screen where a new password can be set and submit it.”
- Persona‑Driven Execution – Each run picks a persona (curious, impatient, novice, adversarial, elderly, accessibility, power user). The persona dictates:
- Tap timing (e.g., impatient users tap quickly, elderly users hold longer).
- Error‑recovery behavior (novice may retry same field multiple times).
- Use of assistive tech (accessibility persona enables VoiceOver and checks announcements).
- Exploration Loop – SUSA interacts with the app, follows links, fills fields, and observes responses (toast, navigation, network calls). It records each state transition and marks dead ends (e.g., a button that does nothing).
- Reporting – At the end of a session, SUSA outputs a JSON report with:
- Discovered paths to the reset screen.
- Success/failure verdict per persona.
- Screenshots of any error UI, crash logs, or accessibility violations.
- Metrics like time‑to‑goal, number of retries, and unique states visited.
Configuring Persona Profiles
You can tune personas via a simple YAML file that SUSA reads at startup. Below is an example that adjusts tap density and enables VoiceOver for the accessibility persona:
personas:
novice:
tap_delay_min: 0.2
tap_delay_max: 0.6
typo_probability: 0.15
use_voiceover: false
impatient:
tap_delay_min: 0.02
tap_delay_max: 0.08
typo_probability: 0.01
use_voiceover: false
accessibility:
tap_delay_min: 0.15
tap_delay_max: 0.4
typo_probability: 0.02
use_voiceover: true
voiceover_rate: 0.5 # slower speech rate for comprehension
power:
tap_delay_min: 0.05
tap_delay_max: 0.15
typo_probability: 0.005
use_voiceover: false
paste_probability: 0.4 # chance to paste from clipboard
Launch SUSA with:
susatest run \
--app /path/to/MyApp.ipa \
--personas-config personas.yaml \
--goal "reset password" \
--output-dir ./susa-reports \
--verbose
Interpreting SUSA Reports
- Success Rate per Persona – If the “adversarial” persona frequently triggers a crash (e.g., by entering extremely long strings or special Unicode), you have a validation‑or‑memory‑issue to fix.
- Dead End Detection – SUSA flags UI elements that never lead to progress (e.g., a “Resend” button that is disabled but never re‑enabled after a timeout). This often points to missing logic for handling server‑side retry‑after headers.
- Accessibility Violations – When the accessibility persona is enabled, SUSA automatically runs AXCore checks and reports missing labels, insufficient contrast, or elements not reachable via swipe navigation.
- Security Hints – The agent logs network requests; if it sees a reset token appearing in plain‑text HTTP logs or being stored in
UserDefaults, you’ll see a warning in the report.
Because SUSA explores without predetermined scripts, it can discover edge cases such as:
- A hidden “Forgot Password?” link that only appears after three failed login attempts (a security‑through‑obscurity trick).
- A reset flow that is launched via a custom URL scheme (
myapp://reset?code=…) that is not advertised anywhere in the UI but is referenced in a push notification payload. - A scenario where switching to a different language (e.g., Right‑to‑Left Arabic) causes the submit button to lose its accessibility label, making it invisible to VoiceOver users.
---
How to Test Forgot Password on iOS (Complete Guide): Accessibility Testing Specifics for Forgot Password
Accessibility is not a nice‑to‑have; it’s a legal requirement in many jurisdictions and directly impacts user trust. The forgot‑password screen often contains custom controls, placeholder text, and dynamic messages that can break VoiceOver, Switch Control, or Dynamic Type. Below is a focused checklist and the tools you can use to validate each item.
VoiceOver & Switch Control
| Check | How to Test | Pass Criteria |
|---|---|---|
All interactive elements have a meaningful accessibilityLabel | Open Accessibility Inspector, enable VoiceOver, swipe to each element. | Label describes purpose (e.g., “Email text field, is editing”). |
| Buttons announce state changes (enabled/disabled) | Toggle network off, attempt submit, listen for “disabled” announcement. | State change is spoken. |
| Alerts and toasts are announced automatically | Trigger a validation error, ensure VoiceOver reads the message without needing to move focus. | Message spoken immediately. |
| Custom transitions do not interfere with navigation | Reduce Motion off, navigate with swipe; ensure focus lands on the next logical element. | Focus moves predictably. |
| The reset link in email opens the app and focus lands on the new‑password field | Use a test mailbox, tap the universal link, then swipe with VoiceOver. | First announced element is the new password field. |
Dynamic Type
- Set the device to Largest Accessibility Size (Settings → Display & Text Size → Larger Text).
- Verify that:
- No text is truncated (use the Accessibility Inspector’s “Show Text Frames” overlay).
- Buttons remain at least 44 × 44 pt tappable area.
- Scrolling works if content overflows (the screen should become scroll‑able, not clipped).
Contrast & Color
- Run the AXContrast tool (built into Xcode) or use the Contrast analyzer in the Accessibility Inspector.
- Ensure that:
- Text vs. background contrast ratio ≥ 4.5:1 for normal text, ≥ 3:1 for large text.
- Placeholder text meets the same ratio (often overlooked).
Assistive Touch & Switch Control
- Enable Switch Control, configure a single switch (e.g., head movement).
- Attempt to complete the reset flow using only the switch.
- Verify that scanning highlights each actionable item and that selecting (auto‑scans after a timeout if no selection is made.
Automated Accessibility Checks
You can integrate XCUITest with the XCUIElement property isAccessibilityElement and run the built‑in XCUITestAccessibility template:
func testAccessibilityLabels() {
let app = XCUIApplication()
app.launch()
let emailField = app.textFields["Email"]
XCTAssertTrue(emailField.isAccessibilityElement)
XCTAssertEqual(emailField.label, "Email address")
let submitBtn = app.buttons["Submit"]
XCTAssertTrue(submitBtn.isAccessibilityElement)
XCTAssertEqual(submitBtn.label, "Send reset link")
}
Or use the open‑source AccessibilityScanner (fastlane plugin) to generate an HTML report after each UI test run
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