How to Test Forgot Password on Android (Complete Guide)

Forgot‑password flow is one of the most frequently used recovery mechanisms in mobile apps. When it fails, users are locked out, support tickets spike, and brand trust erodes. In production, the flow

June 04, 2026 · 18 min read · How-To Guides

Why Forgot Password Testing Matters

Forgot‑password flow is one of the most frequently used recovery mechanisms in mobile apps. When it fails, users are locked out, support tickets spike, and brand trust erodes. In production, the flow often hides subtle bugs that unit tests miss: race conditions between network calls and UI updates, mishandled error messages from the backend, or accessibility barriers that prevent screen‑reader users from completing the reset. Because the flow touches network, credential storage, UI state, and sometimes biometric fallback, a defect can cascade into security issues such as token leakage or brute‑force exposure. A systematic test strategy therefore protects both user experience and application security.

Comprehensive Test Matrix

Below is a matrix that groups test ideas by category, sub‑category, and expected outcome. Use it as a checklist when designing manual or automated suites. Each row can be expanded into a test case with pre‑conditions, steps, and verification points.

CategorySub‑categoryTest IDDescriptionExpected Result
Happy PathValid email entryHP‑01User taps “Forgot password”, enters a registered email, submits, receives success toast, and sees a confirmation screen.Success message displayed, email sent (verify via test mailbox).
Valid phone entry (if supported)HP‑02Same as HP‑01 but using phone number.Success message, SMS sent.
Deep link from emailHP‑03User clicks reset link in email, lands on password‑set screen, enters new password twice, submits.Password updated, redirected to login screen with fresh credentials.
Error PathsUnregistered emailEP‑01Enter email not associated with any account.App shows “No account found” or similar, does not leak existence of account.
Malformed emailEP‑02Enter “user@”, “@@”, or empty string.Inline validation error appears before submission.
Network timeoutEP‑03Simulate latency >10 s or drop connection after submit.App shows retryable error, does not crash, allows user to retry.
Server error 500EP‑04Backend returns HTTP 500.Generic error toast, no stack trace shown to user.
Rate‑limit hitEP‑05Submit request 5 times quickly (if limit is 4/min).App shows “Too many attempts, try later” and blocks further submissions for the cooldown period.
Edge CasesSpecial characters in emailEC‑01Email contains ‘+’, ‘‑’, ‘.’, Unicode (e.g., 用户@例子.cn).Submission succeeds if backend accepts; validation does not reject incorrectly.
Leading/trailing spacesEC‑02User pastes “ user@example.com ” with spaces.App trims internally or shows validation error; never sends spaces to server.
Clipboard pasteEC‑03User pastes email from clipboard that includes hidden characters.App sanitizes input; no crash.
Orientation change mid‑flowEC‑04Rotate device after entering email but before submitting.Entered text persists, UI adapts, no loss of state.
Multi‑window modeEC‑05Launch app in split‑screen, start forgot‑password flow in one pane.Flow works independently, no interference with other pane.
Biometric fallbackEC‑06Device has fingerprint enabled; user cancels biometric prompt and falls back to password entry for reset confirmation.Flow continues with manual entry, no dead end.
AccessibilityTalkBack navigationAC‑01Enable TalkBack, navigate to “Forgot password” button, activate, fill fields, submit.All controls announced, hints appropriate, focus moves correctly after each action.
Contrast ratioAC‑02Verify text and button colors meet WCAG AA (≥4.5:1).Contrast passes automated check (e.g., using Android Accessibility Test Framework).
Touch target sizeAC‑03Ensure tappable elements ≥48 dp.No false positives from lint rules.
Error announcementAC‑04Trigger validation error; TalkBack announces error message.Error spoken immediately when field loses focus.
Security & PrivacyToken leakage in logsSP‑01Capture logcat while submitting reset request.No password reset token, email, or user‑ID appears in plain text.
Secure storage of temporary tokenSP‑02After successful email submission, check app’s SharedPreferences or Keystore for any persisted reset token.Token is not stored; only transient in‑memory variable.
Brute‑force protectionSP‑03Automated script attempts 20 rapid resets with different emails.Backend enforces rate limit or CAPTCHA after threshold; app does not expose timing differences that aid enumeration.
Email enumeration mitigationSP‑04Compare response times and messages for registered vs. unregistered emails.No distinguishable difference (same UI message, similar timing).
HTTPS enforcementSP‑05Use a proxy to attempt HTTP request to reset endpoint.Request is blocked or upgraded to TLS; no fallback to plaintext.

How to Use the Matrix

Manual Testing Approach

Setup

  1. Test environment – Use a physical device or emulator running Android 9 (API 28) or higher.
  2. Test accounts – Create at least two accounts in the backend: one with a known email/phone, one without.
  3. Mail/SMS catcher – Configure a service like Mailinator, Ethereal Email, or a local SMTP server to capture reset messages.
  4. Logging tools – Enable adb logcat with filters for your app’s tag and for NetworkSecurityConfig if you use clear‑text traffic debugging.
  5. Accessibility scanner – Install the Android Accessibility Test Framework (ATF) or use TalkBack directly.

Step‑by‑Step Procedure

  1. Launch the app – Ensure you are on the login screen.
  2. Trigger forgot‑password – Tap the “Forgot password?” link/button. Verify that the navigation animation completes and the focus lands on the email/phone input field (check with TalkBack or adb shell uiautomator dump).
  3. Happy path
  1. Error paths – Repeat steps 2‑3, substituting the test data from the EP rows. For each, verify that:
  1. Edge cases – Perform EC‑01 through EC‑06. Pay special attention to:
  1. Accessibility checks
  1. Security & privacy checks

Document each step with screenshots or short video clips; attach the logcat snippets for any failure. This manual baseline becomes the oracle for automated tests.

Automated Testing on Android

Appium Basics

Appium drives the UI via the UiAutomator2 backend, making it suitable for cross‑framework (Java/Kotlin, Flutter, React Native) apps.


// ForgotPasswordTest.java
import io.appium.java_client.AppiumDriver;
import io.appium.java_client.MobileElement;
import io.appium.java_client.android.AndroidDriver;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.remote.DesiredCapabilities;
import java.net.URL;
import java.util.concurrent.TimeUnit;

public class ForgotPasswordTest {
    private AppiumDriver<MobileElement> driver;

    @Before
    public void setUp() throws Exception {
        DesiredCapabilities caps = new DesiredCapabilities();
        caps.setCapability("platformName", "Android");
        caps.setCapability("deviceName", "Pixel_4_API_33");
        caps.setCapability("appPackage", "com.example.myapp");
        caps.setCapability("appActivity", ".ui.LoginActivity");
        caps.setCapability("automationName", "UiAutomator2");
        driver = new AndroidDriver<>(new URL("http://localhost:4723/wd/hub"), caps);
        driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
    }

    @After
    public void tearDown() {
        if (driver != null) {
            driver.quit();
        }
    }

    @Test
    public void testHappyPath() {
        // Navigate to forgot password
        MobileElement forgotBtn = driver.findElement(By.id("forgot_password_link"));
        forgotBtn.click();

        // Enter email
        MobileElement emailField = driver.findElement(By.id("email_input"));
        emailField.sendKeys("testuser@example.com");

        // Submit
        MobileElement submitBtn = driver.findElement(By.id("send_reset_button"));
        submitBtn.click();

        // Verify toast
        MobileElement toast = driver.findElement(By.xpath("//android.widget.Toast[@text='Reset link sent']"));
        assert toast.isDisplayed();

        // In a real test you would poll a mail server here; omitted for brevity.
    }
}

Key points

Espresso UI Tests

Espresso runs within the same process as the app, giving fast feedback and direct access to ViewMatchers. It is ideal for regression suites that run on every PR.


// ForgotPasswordTest.kt
@LargeTest
@RunWith(AndroidJUnit4::class)
class ForgotPasswordTest {

    @get:Rule
    val activityRule = ActivityScenarioRule(LoginActivity::class.java)

    @Test
    fun `happy path reset password`() {
        // Click forgot password
        onView(withId(R.id.forgot_password_button)).perform(click())

        // Type email
        onView(withId(R.id.email_edit_text))
            .perform(replaceWith("valid@example.com"), closeSoftKeyboard())

        // Submit
        onView(withId(R.id.send_button)).perform(click())

        // Verify toast
        onView(withText("Reset link sent"))
            .inRoot(isToast())
            .check(matches(isDisplayed()))

        // In test, trigger deep link to reset screen via Intent
        val resetIntent = Intent(Intent.ACTION_VIEW, Uri.parse("myapp://reset?token=FAKE"))
        activityRule.scenario.onActivity { it.sendBroadcast(resetIntent) }

        // Fill new password
        onView(withId(R.id.password_edit_text))
            .perform(replaceWith("NewP@ssw0rd!"), closeSoftKeyboard())
        onView(withId(R.id.confirm_edit_text))
            .perform(replaceWith("NewP@ssw0rd!"), closeSoftKeyboard())
        onView(withId(R.id.reset_button)).perform(click())

        // Expect navigation to login
        onView(withId(R.id.login_button)).check(matches(isDisplayed()))
    }

    // Helper matcher for toast
    private fun isToast(): Matcher<Root> {
        return object : TypeSafeMatcher<Root>() {
            override fun matchesSafely(root: Root): Boolean {
                return root.windowLayoutParams?.type == TYPE_TOAST
            }
            override fun describeTo(description: Description) {
                description.appendText("is toast")
            }
        }
    }
}

Tips

UIAutomator Scripts

For black‑box testing where you cannot modify the APK, UIAutomator works well. Below is a simple JUnit‑based script that validates the error path for an unregistered email.


// ForgotPasswordErrorTest.java
import androidx.test.uiautomator.By;
import androidx.test.uiautomator.UiDevice;
import androidx.test.uiautomator.UiObject;
import androidx.test.uiautomator.UiObjectNotFoundException;
import androidx.test.uiautomator.UiSelector;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

public class ForgotPasswordErrorTest {
    private UiDevice device;

    @Before
    public void setUp() {
        device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
        // Assume we are already on login screen; otherwise launch via intent
    }

    @After
    public void tearDown() {
        // No special cleanup needed
    }

    @Test
    public void testUnregisteredEmailShowsMessage() throws UiObjectNotFoundException {
        // Click forgot password
        new UiObject(new UiSelector().descriptionContains("Forgot password")).click();

        // Enter email
        UiObject emailField = new UiObject(new UiSelector().resourceId("com.example.myapp:id/email_input"));
        emailField.setText("unknown@domain.com");

        // Submit
        new UiObject(new UiSelector().resourceId("com.example.myapp:id/send_button")).click();

        // Wait for error toast
        UiObject toast = new UiObject(new UiSelector().className("android.widget.Toast"));
        assertTrue(toast.waitForExists(5000));
        String toastText = toast.getText();
        assertTrue(toastText.contains("No account found") || toastText.contains("We could not find"));
    }
}

Run with:


adb install -r app-debug.apk
adb shell am instrument -w -r   -e debug false -e class com.example.tests.ForgotPasswordErrorTest androidx.test.runner.AndroidJUnitRunner

CI Integration

  1. Server – Use a Linux VM with Android SDK, emulator (or Firebase Test Lab), Node.js for Appium, and JDK.
  2. Pipeline – Example GitHub Actions snippet:

name: Android Forgot Password Tests

on:
  push:
    branches: [main]
  pull_request:

jobs:
  ui-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Set up JDK
        uses: actions/setup-java@v3
        with:
          distribution: temurin
          java-version: '11'
      - name: Install Android SDK
        uses: android-actions/setup-android@v2
      - name: Start emulator
        run: |
          echo "no" | avdmanager create avd -n test -k "system-images;android-33;google_apis;x86_64"
          emulator -avd test -no-window -no-audio &
          ./adb wait-for-device
          ./adb shell input keyevent 82 # Menu
      - name: Run Appium tests
        run: |
          npm ci
          npx appium &
          ./mvnw test -Dtest=ForgotPasswordTest
      - name: Run Espresso tests
        run: ./gradlew connectedAndroidTest

Adjust the steps for your build system (Gradle, Maven, Bazel). The key is to start the emulator or use a cloud device farm, launch Appium (if needed), and execute the test suites. Collect JUnit reports and publish them as artifacts for triage.

Production‑Only Edge Cases

Some defects surface only when the app runs under real‑world conditions: fluctuating networks, localized resources, or device‑specific behaviors. Below are concrete scenarios and how to reproduce them in a controlled lab.

IssueTriggerObservationMitigation
Network flakinessUse tc or netem to add 200 ms latency and 5 % packet loss after the reset request is sent.App shows indefinite spinner, eventually crashes with NullPointerException when trying to parse a partial JSON response.Implement proper timeout handling, show retry UI, and validate JSON schema before use.
Backend rate limitingSend 8 reset requests within 30 seconds (limit set to 5/min).After the 5th request, the backend returns HTTP 429; the app displays raw HTML error page instead of a user‑friendly message.Map HTTP 429 to a localized “Too many attempts” toast and disable the submit button for the cool‑down period.
Localized stringsChange device language to Arabic (right‑to‑left).The email field hint is misaligned, and the “Send” button overlaps the input, causing a missed tap.Use android:autoSizeTextType="uniform" and test with RTL locales; ensure layout uses start/end not left/right.
Biometric fallback raceDevice has fingerprint enabled; user cancels the biometric prompt after 2 seconds, then quickly types email.App temporarily disables the email field, leading to a stuck UI where the keyboard never appears.Decouple biometric flow from UI state machine; always re‑enable inputs on cancel.
Backup restoreUser backs up app data via ADB, wipes device, restores backup, then attempts forgot‑password.Restored SharedPreferences contain a stale reset token; app tries to reuse it and sends malformed request, causing 400 error.Clear any transient auth tokens on app start; do not persist reset‑related data across sessions.
Deep link hijackingMalicious app registers same scheme (myapp://) and intercepts the reset link.Reset token is leaked to the other app; the legitimate app never receives the intent.Use Android App Links with domain verification, or Firebase Dynamic Links, and verify the incoming intent’s package matches yours.
Storage scarcityFill device storage to <10 MB before triggering reset.App fails to write temporary crypto material to cache, throws IOException and crashes.Catch low‑storage exceptions, inform user to free space, and fallback to in‑memory operations where possible.

Reproducing in CI

Automate these as separate test suites tagged prod-edge; run them nightly rather than on every commit to keep feedback loops fast.

Accessibility and Security Considerations

WCAG Checks

WCAG criterionHow to test on AndroidPass condition
1.3.1 Info and RelationshipsEnable TalkBack, navigate to forgot‑password screen; verify that labels (content-desc) correctly describe input fields and buttons.All controls announce purpose; no ambiguous “button” without context.
1.4.3 Contrast (Minimum)Use Android Studio’s Layout Inspector or the accessibility-test-framework to compute contrast ratios for text vs. background.Ratio ≥ 4.5:1 for normal text, ≥ 3:1 for large text.
2.4.7 Focus VisibleWith TalkBack off, use Tab key (via a keyboard) or D‑pad to move focus; ensure a visible highlight appears on interactive elements.Focus outline visible and ≥ 2 dp width.
3.2.1 On FocusChange focus to email field; verify no automatic submission or context change occurs.No unexpected navigation when focus moves.
3.3.2 Labels or InstructionsEnsure each input field has an associated android:hint or labelFor attribute.Hint present and announced by TalkBack.
4.1.2 Name, Role, ValueInspect UI hierarchy via uiautomator dump; confirm that editable fields have role EDITTEXT and buttons have role BUTTON.Roles correctly exposed.

Automate these checks with the Android Accessibility Test Framework (A11yTest) which can be added as an instrumentation test:


@RunWith(AndroidJUnit4::class)
class AccessibilityTest {
    @get:Rule
    val activityRule = ActivityScenarioRule(ForgotPasswordActivity::class.java)

    @Test
    fun forgotPasswordScreenIsAccessible() {
        onView(withId(R.id.email_input)).check(matches(isDisplayed()))
        // Use the A11yTest library
        AccessibilityValidator.validate(activityRule.scenario)
    }
}

Security Checks

CheckMethodExpected outcome
Token in memory onlyUse Android Studio’s Memory Profiler to allocate a large heap, trigger reset, then dump heap (adb shell am dumpheap /data/local/tmp/heap.hprof). Analyze with MAT; ensure no plain‑text token strings appear.No token strings in heap dump.
Secure networkRun `adb logcatgrep -i "Cleartext"` while the app performs the reset request.No clear‑text warnings; all traffic uses TLS.
No logging of PIIAdd a logcat filter for your app’s tag and search for email patterns (\b[\w.+-]+@[\w-]+\.[\w.-]{2,}\b).Zero matches after submitting reset request.
Rate limiting on clientInstrument a click‑counter on the submit button; after 5 rapid clicks, verify that the button becomes disabled and a cooldown timer is shown.Button disabled, timer visible.
Certificate pinning validation (if used)Burp Suite with a custom CA; attempt to intercept reset request.Connection fails, app shows network error, not the reset UI.

Implement these as unit tests where possible (e.g., using MockWebServer to assert that the Authorization header never contains a reset token) and as UI tests for client‑side enforcement.

Autonomous Persona‑Driven Exploration

How Personas Work

Autonomous QA platforms simulate real‑world users by defining personas—behavioral profiles that influence how the agent interacts with the app. Each persona decides:

When the agent encounters a forgot‑password entry point, it will follow the persona’s policy: a curious user may try multiple email formats, an impatient user may spam the submit button, an elderly user may increase font size and rely on voice input, and an adversarial user may attempt SQL‑injection strings or extremely long payloads to probe for crashes.

What SUSA Adds

SUSA (SUSATest) is an autonomous QA agent that you can point at an APK or a web URL. After you upload the build, it:

  1. Discovers the forgot‑password flow without any test scripts—by following links, interpreting UI labels, and trying typical recovery patterns.
  2. Executes the flow through each of its built‑in personas, collecting metrics such as success rate, time to completion, and occurrence of crashes or ANRs.
  3. Flags anomalies that are invisible to scripted tests: e.g., a persona that pastes from clipboard triggers a hidden NullPointerException when the app fails to sanitize hidden Unicode characters; an impatient persona’s rapid taps reveal a race condition where the submit button remains enabled while a network request is in flight, allowing duplicate requests.
  4. Generates regression scripts (Appium for Android, Playwright for Web) that capture the exact interaction sequences that caused the failure, giving developers a reproducible starting point.
  5. Learns across runs: if a particular screen was marked as a dead end by the novice persona, subsequent runs skip exploring it unless a new UI change suggests a revised path.

Because the agent does not rely on pre‑written test cases, it can surface bugs that only appear under atypical usage patterns—precisely the kind of issues that escape manual checklists and automated regression suites that follow a happy‑path script.

Example Findings from a Real‑World Run

During a recent exploration of a fintech app’s forgot‑password flow, SUSA’s adversarial persona submitted the following payload in the email field:


' OR 1=1;--  

The app passed the string straight to the backend’s authentication service, which returned a SQL error that was rendered in the UI as a toast:


SQL syntax error near '' OR 1=1;--' at line 1

This exposed an injection vector that had not been caught by unit tests because the validation layer only checked for an @ symbol. The elderly persona, meanwhile, increased the system font scale to 200 % and triggered a layout overflow where the “Send” button moved off‑screen, making the flow impossible to complete without scrolling—a bug missed in manual testing because testers used the default font size.

The impatient persona’s rapid double‑tap on the submit button after the first request was still pending caused the app to queue two identical network calls. The backend, lacking idempotency checks, created two reset tokens, and the first email arrived while the second overwrote the token in SharedPreferences, leading to a confusing the user could not be used.

These findings were captured as video traces, and automatically generated Appium scripts that reproduced locally. The value validation ** debouncing the button.

Integrating Persona‑Driven Tests

  1. Upload APK to the SUSA CLI: susatest-agent run

run

expired before the user could use it, leading to a false‑negative recovery experience.

SUSA’s report highlighted these three defects, each with a short video clip, logcat excerpt, and the exact UI interaction sequence. The development team used the generated Appium script to add a regression test that:

How to Leverage Autonomous Exploration in Your Workflow

Checklist and Takeaways

Quick Reference Checklist

✅ ItemDescription
Happy pathValid email/phone → success toast → reset link → password change → login with new credentials.
Error handlingUnregistered, malformed, empty inputs show inline validation; no account enumeration via timing or message differences.
Network resilienceTimeout, 5xx, 429 responses produce user‑friendly UI; no crash or stack trace leak.
Edge case inputsSpecial characters, Unicode, leading/trailing spaces, clipboard paste, rotation, multi‑window, biometric fallback handled gracefully.
AccessibilityTalkBack navigation, sufficient contrast, ≥ 48 dp touch targets, error announcements, focus order logical.
SecurityNo tokens or PII in logs, no clear‑text traffic, tokens kept in memory only, rate‑limited, HTTPS enforced, App Links verified.
LocalizationTest with at least one RTL language and one non‑Latin script; layout does not break.
Device statesLow storage, battery saver, doze mode, and background restrictions do not block the flow.
ObservabilityCapture logcat, network traces (via adb shell tcpdump or Stetho), and UI hierarchy on failure for rapid triage.
AutomationMaintain at least one Appium script for happy path, one Espresso suite for error paths, and a UIAutomator script for edge cases; run on every PR.
Persona testingPeriodically run an autonomous agent (e.g., SUSA) to discover hidden regressions; add its generated scripts to the regression suite.

Core Takeaways

  1. Forgot‑password is a security‑sensitive recovery path; treat it with the same rigor as login or payment flows.
  2. Combine deterministic and exploratory testing: scripted suites guarantee regression coverage; persona‑driven agents uncover the “unknown unknowns” that only appear under atypical usage.
  3. **Automate the

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