How to Test Forgot Password on Flutter (Complete Guide)
Testing the forgot‑password flow in a Flutter application is not a nice‑to‑have; it is a critical gate that prevents account lockout, credential leakage, and frustrated users. This guide walks you thr
How to Test Forgot Password on Flutter (Complete Guide)
Testing the forgot‑password flow in a Flutter application is not a nice‑to‑have; it is a critical gate that prevents account lockout, credential leakage, and frustrated users. This guide walks you through why the flow matters, what typically breaks in production, a exhaustive test matrix, manual and automated techniques, concrete Flutter code examples, and how autonomous persona‑driven exploration (such as that offered by SUSA) surfaces bugs that scripted tests miss. Follow each section, adapt the snippets to your project, and use the checklist at the end to verify coverage before every release.
---
Why Forgot Password Testing Matters in Flutter Apps
Forgot‑password screens are often the last resort for users who cannot recall their credentials. If the flow fails, users abandon the app, support tickets spike, and brand trust erodes. In Flutter, the UI is built with a single codebase that targets iOS, Android, web, and desktop, which means a bug can appear on any platform without platform‑specific warnings. Common consequences of an untested flow include:
- Silent failures – the API returns an error but the UI shows a generic “try again” message, leaving users unaware that their email was never sent.
- Unintended navigation – a misplaced
Navigator.poporpushReplacementcan drop the user onto a login screen that still thinks they are authenticated, causing session confusion. - Accessibility gaps – missing semantics or insufficient contrast prevent screen‑reader users from completing the reset.
- Security oversights – lack of rate limiting, token leakage in logs, or exposure of reset links via screenshot‑friendly URLs.
Because Flutter compiles to native ARM code, debugging a crashed reset flow often requires reproducing the exact widget state and network mock, which is hard to do after the fact. Proactive testing catches these issues before they reach users.
---
Common Failure Modes in Production
Understanding what breaks helps you prioritize test cases. The following patterns appear repeatedly in Flutter apps that ship forgot‑password screens without dedicated validation:
| Failure Category | Typical Symptom | Root Cause in Flutter Code |
|---|---|---|
| Network handling | No email received; spinner never stops | Forgetting to await the Future returned by http.post, or ignoring socketException |
| State management | Email field clears after submission, but UI shows stale error | Using setState incorrectly or mutating a ChangeNotifier without notifyListeners |
| Navigation | User lands on a blank screen after submitting email | Calling Navigator.of(context).pop() when the reset route is not on the stack |
| Form validation | Submitting with empty email triggers server error instead of client‑side validation | Missing FormFieldValidator or relying solely on server response |
| Accessibility | TalkBack skips the “Send reset link” button | Omitting semanticsLabel or semanticsButton properties |
| Security | Reset token appears in DevTools console logs | Printing the full response body with debugPrint in production builds |
| Throttling | Rapid successive taps cause multiple emails, enabling abuse | No debounce or rate‑limit logic on the submit button |
| Platform‑specific UI | Overflowing text on iOS due to hardcoded padding | Using fixed EdgeInsets instead of MediaQuery‑based spacing |
Each of these items maps directly to a test case in the matrix below.
---
Comprehensive Test Matrix
The matrix groups test cases by objective, description, expected outcome, and priority (P0 = blocker, P1 = high, P2 = medium). Use it as a reference when writing manual scripts, widget tests, or integration scenarios.
| ID | Objective | Description | Expected Result | Priority |
|---|---|---|---|---|
| FP‑01 | Happy path | Valid registered email entered, submit button tapped | Email sent confirmation dialog appears; backend receives request with correct email; user redirected to “Check your inbox” screen | P0 |
| FP‑02 | Invalid format | Email field contains “notanemail” | Inline validation shows “Please enter a valid email”; submit remains disabled | P0 |
| FP‑03 | Empty field | Submit tapped with blank email | Validation error “Email is required” appears; no network call | P0 |
| FP‑04 | Unregistered email | Email that does not exist in user store | Generic message “If the email exists, you will receive a reset link” (no user enumeration) | P1 |
| FP‑05 | Network timeout | Simulated 10‑second delay or loss of connectivity | Loading spinner shows, then timeout error banner with retry option | P1 |
| FP‑06 | Server error 500 | Backend returns internal error | Error banner displays “Something went wrong. Please try again later.”; no navigation away from reset screen | P1 |
| FP‑07 | Duplicate rapid taps | User taps submit 5 times within 2 seconds | Only one network request sent; UI shows single loading state; no duplicate emails | P2 |
| FP‑08 | Accessibility – label | ScreenReader focuses on email field | Announces “Email address, text field, required” | P1 |
| FP‑09 | Accessibility – contrast | Button background #E0E0E0 on white | Contrast ratio ≥ 4.5:1 (AA) for normal text | P1 |
| FP‑10 | Security – token exposure | Reset link returned in API response | Link never printed to console or logged in release build; appears only in secure storage | P1 |
| FP‑11 | Navigation – back button | User presses device back after entering email | Returns to login screen; email field cleared | P2 |
| FP‑12 | Orientation change | Device rotated while keyboard open | UI adapts; email field remains focused; no loss of entered text | P2 |
| FP‑13 | Internationalization | App set to Arabic (RTL) | Layout mirrors correctly; placeholders and validation messages appear in Arabic | P2 |
| FP‑14 | Dark mode | System theme dark | Text and button colors adapt; contrast remains compliant | P2 |
| FP‑15 | Web specific | Running in Chrome, user pastes email via Ctrl+V | Paste works; validation triggers on change; submit enabled when valid | P2 |
| FP‑16 | Desktop specific | Running on macOS, user tabs through fields | Tab order follows logical flow; activation via Enter works | P2 |
| FP‑17 | Error message localization | Backend returns error code INVALID_TOKEN | UI shows localized message “The link has expired. Request a new one.” | P2 |
| FP‑18 | Rate limiting (security) | Six rapid submissions with same email | After 5th attempt, UI shows “Too many requests. Try again later.” and disables submit for 30 s | P1 |
| FP‑19 | Test data isolation | Using a mock server, ensure no real user data is hit | All requests go to http://localhost:8080/mock/reset; no calls to production endpoints | P0 |
| FP‑20 | Cleanup after test | Test finishes, ensure no pending streams or timers | No memory leak reported by Flutter DevTools; all StreamSubscriptions cancelled | P2 |
You can copy this table into a spreadsheet or test‑management tool and tick off each item as you automate or manually verify it.
---
Manual Testing Approach
Even with automation, a manual exploratory pass catches context‑sensitive issues such as overlapping keyboards, platform‑specific gestures, or visual glitches that only appear under certain zoom levels. Follow this step‑by‑step script on a physical device or emulator for each platform you support.
- Setup
- Install the latest debug build.
- Ensure a test user exists with known email (e.g.,
test@example.com) and password. - Enable “Show layout bounds” in Developer options to spot overflow.
- Turn on TalkBack (Android) or VoiceOver (iOS) for accessibility checks.
- Navigate to Forgot Password
- From the login screen, tap the “Forgot password?” link.
- Verify the transition animation and that the new screen’s app bar shows “Reset Password”.
- Validate UI Elements
- Confirm the email field has a hint text, is outlined, and shows the keyboard when tapped.
- Ensure the submit button is disabled initially.
- Check that a “Cancel” or “Back” button exists and returns to login.
- Happy Path
- Type a valid registered email.
- Observe the submit button enabling.
- Tap submit.
- Verify a loading indicator appears, then a success dialog with message “We’ve sent a reset link to your email”.
- Confirm the dialog dismisses on tap and returns to login screen.
- Error Paths
- Clear the field, tap submit → validation error.
- Enter an malformed email → inline error.
- Enter an unregistered email → generic success‑like message (no enumeration).
- Simulate network loss (enable airplane mode) → timeout banner with retry.
- Restore network, tap retry → success.
- Edge Cases
- Rotate device while keyboard is open → ensure email persists and layout adjusts.
- Rapidly tap submit five times → monitor network log (via
flutter logs) for duplicate calls. - Long‑press the email field → ensure paste works and triggers validation.
- Switch to dark mode → verify colors and contrast.
- Change language to a right‑to‑left locale → confirm mirroring.
- Accessibility Checks
- With TalkBack enabled, swipe to each element and listen for announcements.
- Verify that the submit button is announced as a button, not just a label.
- Ensure that error messages are live‑region announced when they appear.
- Security Spot‑Check
- Enable USB debugging and run
adb logcat(Android) or console logs (iOS/macOS). - Submit a valid email and confirm that the reset token or link never appears in the logs.
- Check that the API request headers do not accidentally include auth tokens.
- Cleanup
- After each scenario, force‑stop the app and relaunch to verify no stale state persists (e.g., lingering loading spinner).
Document any deviation from the expected results in a bug ticket, referencing the matrix ID (e.g., FP‑05). Manual testing is time‑consuming but invaluable for catching UI‑thread timing issues and platform quirks.
---
Automated Testing with Flutter Widget Tests
Widget tests run fast, render the widget tree in a headless environment, and let you stub network layers. They are ideal for validating form logic, validation, and UI state transitions without needing a device.
#### Setting Up the Test Environment
Add the following to dev_dependencies in pubspec.yaml:
dev_dependencies:
flutter_test:
sdk: flutter
mockito: ^5.4.0 # or use mocktail
http: ^1.1.0
Create a test file forgot_password_test.dart under test/.
#### Mocking the Authentication Service
Assume a service class AuthService with a method Future. We'll mock it using Mockito.
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:your_app/ui/forgot_password_page.dart';
import 'package:your_app/services/auth_service.dart';
class MockAuthService extends Mock implements AuthService {}
void main() {
late MockAuthService mockAuth;
late ForgotPasswordPage page;
setUp(() {
mockAuth = MockAuthService();
page = ForgotPasswordPage(authService: mockAuth);
});
testWidgets('shows loading indicator when reset link sent', (WidgetTester tester) async {
// Arrange: mock service returns a completed future
when(mockAuth.sendResetLink(any)).thenAnswer((_) async => Future.value());
await tester.pumpWidget(MaterialApp(home: page));
// Act: enter email and tap submit
await tester.enterText(find.byKey(const Key('emailField')), 'user@example.com');
await tester.tap(find.byKey(const Key('submitButton')));
await tester.pump(); // start loading
// Assert: loading indicator visible
expect(find.byType(CircularProgressIndicator), findsOneWidget);
});
}
This test verifies that the UI reacts correctly to a pending request.
#### Testing Validation Logic
testWidgets('disables submit when email empty or invalid', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(home: page));
// empty field
expect(tester.widget<ElevatedButton>(find.byKey(const Key('submitButton'))).enabled, isFalse);
// invalid email
await tester.enterText(find.byKey(const Key('emailField')), 'notanemail');
await tester.pump();
expect(find.textContaining('Please enter a valid email'), findsOneWidget);
expect(tester.widget<ElevatedButton>(find.byKey(const Key('submitButton'))).enabled, isFalse);
// valid email enables button
await tester.enterText(find.byKey(const Key('emailField')), 'valid@domain.com');
await tester.pump();
expect(tester.widget<ElevatedButton>(find.byKey(const Key('submitButton'))).enabled, isTrue);
});
#### Simulating Network Failure
testWidgets('shows error on network timeout', (WidgetTester tester) async {
// mock a timeout exception
when(mockAuth.sendResetLink(any)).thenThrow(SocketException('Failed host lookup'));
await tester.pumpWidget(MaterialApp(home: page));
await tester.enterText(find.byKey(const Key('emailField')), 'user@example.com');
await tester.tap(find.byKey(const Key('submitButton')));
await tester.pump(const Duration(seconds: 2)); // allow timeout to propagate
expect(find.textContaining('Unable to send reset link'), findsOneWidget);
expect(find.byType(CircularProgressIndicator), findsNothing);
});
#### Testing Navigation After Success
testWidgets('navigates to check‑inbox page on success', (WidgetTester tester) async {
when(mockAuth.sendResetLink(any)).thenAnswer((_) async => Future.value());
await tester.pumpWidget(MaterialApp(
home: Builder(
builder: (context) => page,
),
));
await tester.enterText(find.byKey(const Key('emailField')), 'user@example.com');
await tester.tap(find.byKey(const Key('submitButton')));
await tester.pumpAndSettle();
// Assuming the app uses Navigator.pushNamed
expect(find.text('Check your inbox'), findsOneWidget);
});
These widget tests cover the happy path, validation, error handling, and navigation. Run them with flutter test and integrate them into your CI pipeline to catch regressions on every push.
---
Automated Testing with Flutter Integration Tests
Widget tests are excellent for unit logic, but integration tests validate the full navigation stack, real platform behavior, and asynchronous timings (e.g., debouncing). Use the integration_test package.
#### Adding the Dependency
dev_dependencies:
integration_test:
sdk: flutter
Create integration_test/forgot_password_test.dart.
#### Basic Integration Test Skeleton
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:your_app/main.dart' as app;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('Forgot Password Flow', () {
testWidgets('end‑to‑end success scenario', (WidgetTester tester) async {
app.main(); // launches the app
await tester.pumpAndSettle();
// 1. Navigate to forgot password
await tester.tap(find.text('Forgot password?'));
await tester.pumpAndSettle();
// 2. Fill email
await tester.enterText(find.byKey(const Key('emailField')), 'test@example.com');
await tester.pump();
// 3. Submit
await tester.tap(find.byKey(const Key('submitButton')));
await tester.pumpAndSettle();
// 4. Verify success dialog
expect(find.textContaining('We’ve sent a reset link'), findsOneWidget);
await tester.tap(find.text('OK'));
await tester.pumpAndSettle();
// 5. Ensure we are back on login
expect(find.text('Login'), findsOneWidget);
});
});
}
Run with flutter drive --target=integration_test/forgot_password_test.dart -d .
#### Adding Network Mocking for Integration Tests
For true isolation, spin up a mock server (e.g., using mocktail + shelf) on a localhost port and configure the app to point to it via environment variables or a flavors file.
// In main.dart, before runApp
final String apiBase = String.fromEnvironment('API_BASE', defaultValue: 'https://api.example.com');
// Then pass apiBase to your service constructor.
In CI, set API_BASE=http://10.0.2.2:8080/mock (Android emulator host alias) to route calls to your mock.
#### Testing Debounce / Rate Limiting
testWidgets('prevents duplicate submissions within 5 seconds', (WidgetTester tester) async {
app.main();
await tester.pumpAndSettle();
await tester.enterText(find.byKey(const Key('emailField')), 'user@example.com');
await tester.tap(find.byKey(const Key('submitButton')));
await tester.pump(); // first request starts
// immediate second tap
await tester.tap(find.byKey(const Key('submitButton')));
await tester.pump(); // should not start a second request
// verify only one call was made (you can expose a counter via a mock service)
expect(mockCallCount, equals(1));
});
Integration tests catch timing‑dependent bugs such as race conditions between keyboard dismissal and navigation, which widget tests may miss.
---
Leveraging Autonomous Persona‑Driven Exploration (SUSA)
Scripted tests excel at verifying known scenarios, but they rarely venture into the “what if” space where real users behave unpredictably. Autonomous QA platforms like SUSA explore an app without predefined scripts, simulating a variety of user personas (curious, impatient, novice, adversarial, elderly, accessibility‑focused, power user, etc.) and exercising the UI through taps, scrolls, text entry, and dialog handling.
When you point SUSA at a Flutter build (APK, IPA, or web URL) and let it run a session, it will:
- Discover the forgot‑password link even if it is tucked inside a dialog or hidden behind a profile icon.
- Vary entry speed – a novice persona may type slowly, pausing between characters, while an impatient persona may rapid‑tap the submit button, exposing missing debounce logic.
- Trigger accessibility flows – the elderly persona uses larger font scaling and screen‑reader navigation, revealing contrast or focus‑order problems that automated tests overlook if they don’t explicitly set
MediaQuery.textScaleFactor. - Attempt adversarial actions – injecting malformed Unicode, extremely long strings, or attempting to bypass rate limits by switching networks mid‑flow.
- Observe cross‑session memory – after a first run, SUSA remembers screens that led to dead ends (e.g., a button that navigates to a blank screen) and avoids them in subsequent runs, focusing effort on unexplored paths.
The outcome is a detailed report that lists:
- Crashes / ANRs captured with stack traces.
- UX friction metrics such as time‑to‑complete, number of mis‑taps, and back‑track counts.
- Accessibility violations (WCAG AA/AAA) with screenshots.
- Security hints like tokens appearing in logs or missing rate‑limit headers.
Because SUSA does not rely on hardcoded locators, it adapts to UI changes automatically—if the forgot‑password button moves from the login screen to a bottom‑sheet, the platform still finds it. This complementary approach catches regressions that slip through scripted suites, especially those tied to platform‑specific gestures or dynamic theming.
To try it locally, install the CLI:
pip install susatest-agent
susatest run --apk path/to/app.apk --personas all --output susa_report.json
Then review the generated JSON or HTML report for any findings related to the forgot‑password flow. Incorporate SUSA runs into your nightly CI as an exploratory gate alongside your unit and integration tests.
---
Accessibility and WCAG Checks for the Forgot‑Password Flow
Beyond the basic TalkBack/VoiceOver smoke test, systematic accessibility validation ensures compliance with WCAG 2.1 AA (and AAA where feasible). Use a combination of automated audits and manual verification.
#### Automated Auditing with flutter_axe
Add the dependency:
dev_dependencies:
flutter_axe: ^4.0.0
Create a test that runs the axe engine on the forgot‑password page:
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_axe/flutter_axe.dart';
void main() {
testWidgets('Forgot password page passes axe audit', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(home: ForgotPasswordPage()));
final axeResult = await axeForAccessibility(tester);
expect(axeResult.isSuccessful, isTrue, reason: axeResult.errorMessage);
});
}
If any rule fails (e.g., color-contrast, label, touch-target-size), the test will surface the exact node and suggestion.
#### Manual Checklist for Accessibility
| Item | How to Verify | Pass Criteria |
|---|---|---|
| Labeling | Enable TalkBack, focus on email field | Announces “Email address, text field, required” |
| Placeholder contrast | Inspect placeholder text color | Minimum 3:1 contrast against background (large text) |
| Touch target size | Use layout bounds overlay | Buttons ≥ 48 dp × 48 dp |
| Error announcement | Trigger validation error | Error message spoken immediately as live region |
| Keyboard navigation | Tab through fields on web/desktop | Logical order, visible focus ring |
| Screen‑reader language | Change device language to Spanish | All announcements in Spanish |
| Reduced motion | Enable “Reduce animations” in system settings | No non‑essential animation plays |
| Dynamic type | Set largest font size | Text scales, layout does not overflow |
Fix any failures before marking the flow as accessible.
---
Security and Privacy Considerations
Forgot‑password mechanisms are a common attack vector for credential harvesting, account enumeration, and token leakage. Address the following points in your test plan and implementation.
| Concern | Test Technique | Expected Outcome |
|---|---|---|
| User enumeration | Submit random non‑existent emails and compare response timing/message with existent emails | Identical generic response (same wording, same timing within ±100 ms) |
| Rate limiting | Automated script sending 20 requests in 5 s from same IP | After threshold (e.g., 5), server returns 429 or UI shows “Too many attempts” and disables submit |
| Token leakage in logs | Enable verbose logging, submit request, inspect logcat/console | No reset token, email, or API key appears in logs |
| HTTPS enforcement | Use a network interceptor (e.g., dio with CertificatePinning) to attempt plain‑http request | Request fails; app does not fallback to clear text |
| Reset link expiration | Request link, wait past expiry (e.g., 24 h), attempt to use link | Server returns “link expired”; UI shows appropriate message |
| Brute‑force protection on token | Attempt to guess a token via brute force (should be infeasible) | Server rate‑limits token validation attempts |
| Privacy of email | Verify that email is not stored in analytics or shared with third‑party SDKs | No network calls to analytics endpoints containing the email after submit |
| CSP / XSS on web | Inject into email field (if allowed) | Input sanitized; script not executed; validation rejects or escapes |
Implement server‑side controls (rate limiting, token entropy, short‑lived tokens) and client‑side guards (debounce, input sanitization, secure storage). Then write tests that assert the absence of the above failure modes.
---
Consolidated Checklist
Use this short list before every release candidate. Tick each item; if any item is unchecked, treat the forgot‑password flow as not ready for release.
- [ ] Happy path: valid email → success dialog → returns to login.
- [ ] Validation: empty, malformed, and unregistered emails produce correct inline messages.
- [ ] Network resilience: timeout, 500, and 429 responses show appropriate UI and retry option.
- [ ] Debounce / rate limiting: rapid taps ≤ 1 request per 5 s; UI shows warning after threshold.
- [ ] Navigation: back button, device back, and orientation changes preserve state and return correctly.
- [ ] Accessibility: TalkBack/VoiceOver announces all elements; contrast ≥ 4.5:1; touch targets ≥ 48 dp.
- [ ] Internationalization: layout mirrors for RTL; translations appear correctly.
- [ ] Dark mode: colors adapt, contrast remains compliant.
- [ ] Web/Desktop: paste, tab, and Enter key work as expected.
- [ ] Security: no token/log leakage, identical responses for existent/non‑existent emails, HTTPS enforced.
- [ ] Automated coverage: widget tests for validation & navigation, integration test for end‑to‑end flow, axe audit passes.
- [ ] Exploratory: SUSA (or similar) run reports no new crashes, ANRs, or accessibility violations in the forgot‑password flow.
If you run the checklist on a physical device matrix (Android 12+, iOS 16+, Chrome, Safari, Edge, macOS, Windows) you will capture platform‑specific regressions early.
---
Closing Takeaways
Testing a forgot‑password screen in Flutter is more than checking that a button changes color; it is a confluence of form validation, network handling, navigation, accessibility, security, and platform‑specific quirks. By following the matrix above, you gain a repeatable way to verify every critical path—from the happy route to obscure edge cases like rapid taps on a low‑end device while TalkBack is active.
Automated widget and integration tests give you fast feedback on logic and timing, while exploratory, persona‑driven tools such as SUSA uncover the hidden gaps that scripts never anticipate—think of a power user who pastes a 500‑character string, or an elderly user who enlarges font size to 200 % and encounters overlapping UI.
Make the forgot‑password flow a first‑class citizen in your test suite, run the checklist on every release, and treat any deviation as a signal to improve both the UI and the backend contract. When the flow is solid, users regain access without friction, support costs drop, and confidence in your application’s reliability grows. Happy testing!
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