How to Test Forgot Password: A Complete Guide

How to Test Forgot Password: A Complete Guide begins with recognizing that the password recovery flow is a critical security and usability gate. A broken or confusing reset process can lock legitimate

April 16, 2026 · 17 min read · How-To Guides

How to Test Forgot Password: A Complete Guide begins with recognizing that the password recovery flow is a critical security and usability gate. A broken or confusing reset process can lock legitimate users out, expose accounts to credential stuffing, or leak sensitive data through side‑channels. Because the flow touches user‑input validation, out‑of‑band communication, token handling, and final password change, it is a prime target for both functional bugs and security flaws. This guide walks you through a complete, platform‑agnostic test matrix, shows how to execute those tests manually and with automation, highlights production‑only gotchas, and provides a reusable checklist you can embed in your release gate.

Why Forgot Password Testing Matters

The forgot‑password pathway is often the only self‑service mechanism users have when they lose access. If it fails, support costs rise, churn increases, and brand trust erodes. From a security perspective, the flow is a gateway to account takeover: weak tokens, missing rate limits, or information disclosure in error messages can let an attacker enumerate valid emails, brute‑force reset codes, or hijack the reset link. Moreover, accessibility regulations (WCAG 2.1 AA) require that the flow be operable via keyboard, screen readers, and assistive tech. Testing the flow therefore covers three intertwined concerns: usability, reliability, and security.

Core Components of a Forgot Password Flow

Understanding the building blocks lets you map test cases to specific code paths and infrastructure pieces.

Entry Point

The user typically clicks a “Forgot password?” link on the login screen or enters an identifier (email, phone, username) on a dedicated recovery page. This step must validate the identifier format, check whether the account exists without revealing that information, and trigger the out‑of‑band message.

Email/SMS Delivery

After validation, the backend generates a one‑time token, stores it (often hashed) with an expiry timestamp, and dispatches a message via an email provider or SMS gateway. The message contains a link or code that the user must return to the app or web portal.

Token Validation

When the user follows the link or enters the code, the backend verifies that the token matches a stored record, has not expired, and belongs to the correct identifier. Successful validation grants access to the reset password form.

Reset Password Form

Here the user supplies a new password, usually with confirmation and strength‑checking logic. The backend then replaces the old credential, invalidates any existing sessions (optional), and notifies the user of success.

Post‑Reset Handling

Some systems log the reset event, send a confirmation email, or force a re‑login. Others may present a success screen with a link back to the login page. This final step ensures the user knows the reset completed and can continue.

Test Matrix: Happy Path, Error Paths, Edge Cases

A systematic matrix helps you avoid missing scenarios. The table below groups tests by component, lists the objective, and gives a concise test description. Use it as a starting point; add project‑specific variations as needed.

ComponentTest CategoryObjectiveTest Description
Entry PointHappy PathVerify correct initiationSubmit a valid, registered email; expect a success message and no UI error.
Entry PointError Path – Invalid FormatReject malformed inputEnter “plaintext”, “@”, or an email missing domain; expect inline validation error.
Entry PointError Path – Unknown IdentifierAvoid account enumerationSubmit an unregistered email; expect generic message like “If the address exists, you will receive an email.”
Entry PointEdge Case – Empty FieldHandle blank submissionLeave identifier empty; expect required‑field warning.
Email/SMS DeliveryHappy PathConfirm dispatchUse a test mailbox (e.g., Mailinator) or a SMS simulator; verify receipt within 5 s.
Email/SMS DeliveryError Path – Provider FailureGraceful degradationSimulate email service downtime (mock 500); expect user‑friendly retry or fallback message.
Email/SMS DeliveryEdge Case – Delayed DeliveryTolerate latencyIntroduce a 30‑second delay in the mock gateway; ensure UI shows “sending…” and does not timeout prematurely.
Token ValidationHappy PathValid token acceptanceClick the link within expiry; reset form loads with pre‑filled identifier.
Token ValidationError Path – Expired TokenReject stale linksWait until token expiry (e.g., 15 min) then click link; expect “link expired” message.
message.
Token ValidationError Path – Tampered TokenDetect manipulationAltering the URL token by one character; expect invalid token error.
Token ValidationEdge Case – Token ReusePrevent replayUse a valid token to reset password, then attempt to reuse same token; expect rejection.
Reset Password FormHappy PathSuccessful password changeEnter a strong new password, confirm, submit; expect success notification and ability to login with new cred.
Reset Password FormError Path – Weak PasswordEnforce policySubmit “12345”; expect inline strength‑violation message.
Reset Password FormError Path – Mismatch ConfirmationCatch typosEnter “Secure!23” and confirm “Secure!24”; expect mismatch warning.
Reset Password FormEdge Case – Password ReuseBlock recent passwordsAttempt to set new password equal to the last two passwords; expect policy error.
Post‑Reset HandlingHappy PathConfirmation communicatedAfter reset, see success screen and receive confirmation email/SMS.
Post‑Reset HandlingError Path – Silent FailureDetect missing notificationDisable outbound mail in test env; submit reset; verify UI still shows success but log shows missing dispatch.
Post‑Reset HandlingEdge Case – Session InvalidationOptional forced re‑loginAfter reset, attempt to use existing auth token; expect 401 or redirect to login.

How to Use the Matrix

  1. Map to automation – Each row can become a parameterized test case (e.g., using TestNG data providers or pytest fixtures).
  2. Prioritize risk – Assign severity: token reuse and weak‑password acceptance are high‑severity; delayed delivery is medium.
  3. Track coverage – Link each row to a test‑case ID in your test‑management tool; ensure at least 95 % of rows are automated before each release.

Accessibility Considerations

Accessibility bugs often hide in plain sight because they do not cause functional failures but prevent users with disabilities from completing the flow.

Screen Reader Compatibility

Keyboard Navigation

Color Contrast and Visual Cues

ARIA Roles and Landmarks

Automated Accessibility Checks

Integrate tools like axe-core, pa11y, or Google’s Lighthouse into CI. A sample Playwright snippet that runs axe on the reset page:


import { test, expect } from '@playwright/test';
import { injectAxe, checkA11y } from 'playwright-axe';

test.describe('Forgot password accessibility', () => {
  test('page passes axe tests', async ({ page }) => {
    await page.goto('/forgot-password');
    await injectAxe(page);
    const results = await checkA11y(page, {
      // exclude known false‑positives if needed
      rules: { 'color-contrast': { enabled: false } }
    });
    expect(results.violations).toEqual([]);
  });
});

Running this on each commit catches regressions early.

Security‑Focused Tests

Security testing for password recovery goes beyond functional checks; it probes for information leakage, token weaknesses, and insufficient throttling.

Rate Limiting and Brute‑Force Protection


for i in {1..20}; do
  curl -s -X POST https://api.example.com/auth/forgot \
    -d "email=user${i}@example.com" \
    -w "%{http_code}\n"
done

If you see differing messages (e.g., “Email not found” vs. “If the address exists…”), you have an enumeration vulnerability.

Token Entropy and Expiry


import secrets, re
token = secrets.token_urlsafe(32)  # example generation
assert len(token) >= 32
assert re.fullmatch(r'[A-Za-z0-9_-]+', token)

Information Leakage in Error Messages


const known = await request.post('/forgot').send({email: 'alice@example.com'});
const unknown = await request.post('/forgot').send({email: 'zzzunknown@example.com'});
expect(known.text).toEqual(unknown.text);

Secure Token Storage


SELECT token_hash FROM password_resets WHERE email = 'test@example.com';
-- Expect output like $2b$12$... (bcrypt) not a raw UUID.

Session Invalidation Post‑Reset

Security Test Checklist (Table)

Security AreaTestPass Criteria
Rate limiting20 rapid varied emailsUniform response, no enumeration
Token entropyToken length ≥ 32 chars, URL‑safeRegex match
Token expiryUse token after expiry“Link expired” message
Token reuseReuse token after successful resetRejected
Error message leakageKnown vs unknown email diffIdentical payloads
Token storageDB lookup shows hash, not plaintextHash format (bcrypt, argon2)
Session invalidationPre‑reset session works post‑reset?According to policy (usually no)
HTTPS enforcementSubmit over HTTPRedirect to TLS or error

Run these tests in a dedicated security test suite, ideally as part of a nightly pipeline or a pre‑release security gate.

Manual Testing Approaches

Even with strong automation, exploratory manual testing catches nuances that scripts miss—especially around UX, confusing copy, and unexpected device behaviors.

Exploratory Checklist

  1. Start from various entry points – login screen, deep link from email, bookmarked recovery URL.
  2. Try atypical identifiers – phone numbers with spaces, plus signs, leading zeros; emails with sub‑addressing (+tag).
  3. Observe copy and tone – Is the message reassuring? Does it avoid technical jargon?
  4. Test interruptions – Switch apps, lock screen, or receive a call while waiting for the SMS/email; does the flow survive?
  5. Check fallback – If email fails, does the UI offer a “Resend” or “Try SMS” option?
  6. Validate post‑reset state – After reset, can you log in with the new password on the same device and on a different device?
  7. Look for leftover debug screens – In staging builds, ensure no verbose error stacks are shown to the user.

Record observations in a shared spreadsheet with columns: *Step, Expected, Actual, Severity, Notes*. This creates a living document that grows with each release.

Session Recording and Playback

Tools like SessionStack, FullStory, or open‑source rrweb let you capture real user sessions. Filter for visits to /forgot-password and watch for:

These insights often reveal friction points that automated checks would deem “pass”.

Proxy and Network Manipulation

Using mitmproxy or Charles Proxy, you can:

Example mitmproxy script to add a 5‑second delay to the forgot‑password endpoint:


def response(flow):
    if flow.request.pretty_url.endswith("/forgot-password"):
        flow.response.stream = True
        flow.response.headers["Cache-Control"] = "no-cache"
        # artificial latency
        import time
        time.sleep(5)

Run the proxy, point your device or emulator to it, and execute the manual steps above.

Automated Testing Strategies

Automation provides repeatability and scalability. Choose the right layer (unit, integration, UI) for each risk area.

Unit and Service Tests

Integrated API Tests

Use Postman, REST Assured, or karate to drive the backend directly.


@Test
void forgotPassword_returnsSameResponseForKnownAndUnknown() {
    Response known = given()
        .body("{\"email\":\"alice@example.com\"}")
        .post("/auth/forgot");
    Response unknown = given()
        .body("{\"email\":\"zzzunknown@example.com\"}")
        .post("/auth/forgot");
    assertEquals(known.asString(), unknown.asString());
}

Add data‑driven files (CSV or JSON) that contain variations: valid, invalid, borderline, and malicious inputs.

UI Tests with Playwright/WebDriver

For web applications, Playwright offers reliable cross‑browser automation. A basic flow:


const { test, expect } = require('@playwright/test');

test.describe('Forgot password UI', () => {
  test('happy path reset', async ({ page }) => {
    await page.goto('/login');
    await page.click('text=Forgot password?');
    await page.fill('#email', 'tester@example.com');
    await page.click('button[type=submit]');
    await expect(page.locator('.success-message')).toHaveText(/We’ve sent an email/i);

    // simulate email extraction (in test env use a mailhook)
    const link = await getResetLinkFromMailbox('tester@example.com');
    await page.goto(link);
    await expect(page).toHaveURL(/\/reset-password\?token=/);
    await page.fill('#password', 'NewStr0ng!Pwd');
    await page.fill('#confirm', 'NewStr0ng!Pwd');
    await page.click('button[type=submit]');
    await expect(page.locator('.success-banner')).toHaveText(/Password updated/i);
    await page.goto('/login');
    await page.fill('#email', 'tester@example.com');
    await page.fill('#password', 'NewStr0ng!Pwd');
    await page.click('button[type=submit]');
    await expect(page).toHaveURL(/^\/dashboard/);
  });
});

Tips for flaky tests

Mobile UI Tests with Appium (Android)


@Test
public void testForgotPasswordFlow() throws Exception {
    driver.findElement(By.id("forgot_password_link")).click();
    driver.findElement(By.id("email_input")).sendKeys("qa@example.com");
    driver.findElement(By.id("submit_button")).click();

    // wait for toast indicating email sent
    new WebDriverWait(driver, 10)
        .until(ExpectedConditions.visibilityOfElementLocated(By.id("toast_sent")));

    // retrieve link from test mailbox (e.g., using MailSlurp API)
    String resetLink = MailSlurp.waitForLatestEmail("qa@example.com")
                                 .getLinkContaining("/reset-password");
    driver.get(resetLink);

    driver.findElement(By.id("password_input")).sendKeys("StrongPass!23");
    driver.findElement(By.id("confirm_input")).sendKeys("StrongPass!23");
    driver.findElement(By.id("reset_button")).submit();

    Assert.assertTrue(driver.findElement(By.id("success_banner")).isDisplayed());
}

Data‑Driven and Property‑Based Testing

Leverage libraries like fast-check (JS) or hypothesis (Python) to generate thousands of identifier strings and assert that the backend never leaks existence information.


from hypothesis import given, strategies as st

@given(st.text(min_size=1, max_size=100))
def test_no_email_enumeration(email):
    r1 = client.post("/forgot", json={"email": email})
    r2 = client.post("/forgot", json={"email": email + "x"})
    assert r1.text == r2.text   # same generic message

Autonomous Exploration with SUSA

SUSA’s agent can be pointed at a staging build (APK or URL) and left to exercise the forgot‑password flow across its built‑in personas. Because it explores without pre‑written scripts, it often discovers:

To run a SUSA session:


pip install susatest-agent
susatest explore --app ./app-staging.apk --personas all --duration 30m --output susa-report.json

The resulting report includes a flow map highlighting which screens were visited, success/failure rates for the reset flow, and any detected crashes or accessibility violations. You can feed the discovered UI sequences back into your test suite as new automated scenarios.

Production‑Only Edge Cases

Certain bugs only manifest when the system faces real‑world traffic, carrier filters, or third‑party service quirks. Planning for them reduces post‑release incidents.

Email Provider Throttling and Spam Filtering

SMS Carrier Filtering and Number Formatting

Locale‑Specific Date/Time and Number Formats

Fallback Mechanisms and Multi‑Channel Delivery

Concurrent Resets and Token Collision

Push‑Notification‑Based Recovery (Alternative to Email/SMS)

Some apps send a push notification with a one‑time code instead of email/SMS. Test:

Checklist for Release

Before tagging a release, run through this concise list. Mark each item as Pass, Fail, or N/A and block promotion on any Fail.

CategoryItemPass Criteria
FunctionalHappy‑path reset works from login page, deep link, and bookmarked URLSuccess message + ability to log in with new cred
FunctionalInvalid email format shows inline errorField‑level validation
FunctionalUnknown email returns generic messageNo enumeration
FunctionalToken expired yields clear error“Link has expired”
FunctionalToken tampering yields error“Invalid link”
FunctionalWeak password rejected per policyInline strength message
FunctionalPassword mismatch caughtConfirmation error
AccessibilityAll form fields have associated labels or aria‑labelsManual inspection + axe passes
AccessibilityLive region announces status updatesScreen‑reader test
AccessibilityContrast ratio ≥ 4.5:1 for textColour contrast analyzer
SecurityRate limiting prevents enumeration (20 varied emails → same response)Burst test
SecurityToken length ≥ 32 chars, URL‑safeRegex check
SecurityToken stored as hash, not plaintextDB inspection
SecurityPost‑reset session invalidated per policyToken reuse test
SecurityError messages identical for known/unknown emailsDiff test
ProductionEmail simulator handles 100‑burst with retry/queueNo dropped messages
ProductionSMS formatter accepts E.164 and rejects malformedCarrier mock
ProductionLocale change does not break expiry displayLocale switch test
ProductionFallback to secondary channel when primary failsMock failure test
UXResend button re‑enables after cooldownTimer test
UxSuccess screen provides clear next step (login link)Manual review
Automation≥ 90 % of matrix rows have automated test coverageTest‑management report
DocumentationRelease notes mention any changes to reset flowChangelog entry

You can embed this checklist in your CI pipeline as a step that reads a JSON/YAML file and fails the build on any unmet criterion.

Takeaways and Continuous Improvement

Testing a forgot‑password flow is more than checking that a link arrives; it is a convergence of input validation, out‑of‑band communication, token security, accessibility, and real‑world service quirks. By structuring your effort around a matrix that separates happy path, error paths, edge cases, accessibility, and security, you gain a clear view of coverage and risk. Manual exploratory sessions—especially when guided by personas—catch UX friction that scripts overlook, while automated unit, API, and UI tests give you regression safety. Production‑only gotchas like carrier filtering, email throttling, and locale‑specific formatting demand dedicated monitoring and fallback strategies. Finally, a lightweight but enforceable checklist turns the matrix into a gate you can apply before every release, ensuring that the password recovery experience stays both usable and secure.

When you integrate these practices—combining thorough test matrices, disciplined manual exploration, layered automation, and vigilant production monitoring—you transform a frequently overlooked feature into a robust trust anchor for your users. Keep the matrix alive: add new rows whenever you discover a novel failure mode, retire those that are mitigated, and let your team’s shared understanding of the flow evolve alongside the application itself.

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