How to Test Biometric Login on Flutter (Complete Guide)
How to Test Biometric Login on Flutter (Complete Guide) starts with understanding why biometric authentication matters for Flutter apps. Modern users expect fast, secure sign‑in methods, and Flutter’s
How to Test Biometric Login on Flutter (Complete Guide) starts with understanding why biometric authentication matters for Flutter apps. Modern users expect fast, secure sign‑in methods, and Flutter’s platform‑channel plugins let you call Android’s BiometricPrompt or iOS’s LocalAuthentication with a single Dart API. When the integration is weak, production crashes, false‑negatives, or privacy leaks appear, eroding trust and triggering store rejections. This guide walks you through a complete test matrix, manual steps, automated strategies, tooling, and how autonomous, persona‑driven exploration surfaces bugs that scripted tests miss.
How to Test Biometric Login on Flutter (Complete Guide): Why It Matters
Biometric login is no longer a nice‑to‑have; it is a feature; it is often the primary factoring. A broken flow can cause:
- Authentication bypass – a malformed cipher or missing fallback lets an attacker sign in without a valid biometric.
- ANR or crash – invoking the biometric API on a background thread or without proper UI handling freezes the app.
- Accessibility failure – missing labels or poor contrast lock out users who rely on screen readers.
- Privacy violation – storing raw biometric data or failing to clear cached keys breaches GDPR or CCPA.
- User friction – forcing a fallback PIN after every biometric failure frustrates power users and increases churn.
Flutter’s local_auth plugin abstracts the native APIs, but the abstraction adds layers where things can go wrong:
- Platform channel misuse – forgetting to await the result or ignoring error codes.
- Incorrect use of
authenticateWithBiometricsvsauthenticateWithBiometricsOrDeviceCredentials– mixing up policies leads to unexpected PIN prompts. - Missing error handling – not distinguishing
BiometricError.authenticationFailedfromBiometricError.lockedOut. - UI thread violations – showing dialogs from a non‑UI thread causing a black screen.
- State mismanagement – letting the login widget rebuild while the biometric prompt is active, resulting in overlapping dialogs.
Understanding these failure modes shapes the test matrix that follows.
How to Test Biometric Login on Flutter (Complete Guide): Test Matrix
A systematic matrix ensures you cover happy paths, error conditions, accessibility, and security. Below is a comprehensive table you can copy into a test plan spreadsheet.
| Category | ID | Description | Preconditions | Steps | Expected Result | Pass/Fail Criteria |
|---|---|---|---|---|---|---|
| Happy Path | HP1 | Successful fingerprint login | Device has enrolled fingerprint, app granted biometric permission | 1. Navigate to login screen 2. Tap “Use Biometrics” 3. Place enrolled finger on sensor | Biometric prompt appears, authentication succeeds, user lands on home screen | PASS if home screen loads within 2 s, no error dialog |
| Happy Path | HP2 | Successful face login (iOS) | Face ID enrolled, permission granted | Same as HP1 but using face | Prompt shows Face ID UI, success, home screen | PASS if no fallback PIN appears |
| Error Path | EP1 | Biometric sensor not available | No fingerprint/face enrolled, biometric hardware present | Tap biometric button | Dialog shows “Biometric not available”, offers PIN/password fallback | PASS if fallback offered and works |
| Error Path | EP2 | User cancels prompt | Sensor available | Tap biometric button, then press cancel in prompt | Prompt dismisses, app shows “Authentication cancelled”, remains on login screen | PASS if app stays on login, no crash |
| Error Path | EP3 | Too many failed attempts (lockout) | Simulate 5 failed attempts (Android) or 5 failed Face ID attempts (iOS) | Repeatedly place wrong finger or look away | After threshold, biometric disabled, prompt shows “Too many attempts”, offers device credentials | PASS if lockout respected and fallback works |
| Edge Case | ED1 | App in background when biometric invoked | App minimized, biometric button still visible (e.g., via notification) | Tap biometric button from notification | System brings app to foreground, prompt appears, behaves as HP1 | PASS if no crash and prompt works |
| Edge Case | ED2 | Screen orientation change during prompt | Start biometric auth, rotate device while prompt showing | Rotate device | Prompt remains visible, no UI tearing, auth result delivered correctly | PASS if auth completes without visual glitch |
| Edge Case | ED3 | Low memory condition | Run app on device with < 500 MB free RAM, start biometric flow | Trigger biometric button | App does not OOM, prompt appears, authentication works | PASS if no OutOfMemoryError |
| Accessibility | AC1 | TalkBack/VoiceOver label | Accessibility service enabled | Navigate to biometric button, activate TalkBack | Button announces “Use biometric login, button” | PASS if label present and descriptive |
| Accessibility | AC2 | Contrast ratio | High contrast theme enabled | Inspect biometric button colors | Contrast ≥ 4.5:1 (AA) for normal text | PASS if meets WCAG AA |
| Accessibility | AC3 | Touch target size | Enable switch control | Measure biometric button hit area | Minimum 48 dp × 48 dp | PASS if meets guideline |
| Security | SE1 | Raw biometric data not stored | Device with root/jailbreak detection disabled | After successful auth, inspect app’s private storage (via adb run-as or Xcode device logs) | No biometric template or raw sensor data found | PASS if only encrypted keys or tokens stored |
| Security | SE2 | Key invalidation on biometric change | Enroll a new fingerprint after successful auth | Attempt to use existing encrypted token | Token invalidated, app prompts for re‑authentication | PASS if old token rejected |
| Security | SE3 | Resistance to replay attack | Capture biometric auth intent via Frida or similar, replay | Send captured intent to app | App rejects replay, shows authentication failure | PASS if replay blocked |
| Privacy | PR1 | Permission rationale shown | First‑time launch, biometric permission not granted | Attempt to use biometric button | System shows permission rationale dialog defined in AndroidManifest.xml or Info.plist | PASS if rationale appears and is concise |
| Privacy | PR2 | No biometric data in logs | Enable verbose logging, attempt auth | Search logs for keyword “fingerprint” or “face” | No raw biometric data appears | PASS if logs contain only result codes |
Each row maps to a concrete test you can automate or perform manually. The matrix is deliberately exhaustive; you can prune low‑risk rows for rapid regression cycles but keep the core happy‑path, error‑path, accessibility, and security rows for every release.
How to Test Biometric Login on Flutter (Complete Guide): Manual Testing Approach
Manual testing remains valuable for exploratory checks, especially for edge cases that depend on device state or user perception. Follow this step‑by‑step checklist on a physical device (emulators often lack genuine biometric hardware).
Setup
- Enroll at least one biometric credential (fingerprint or face) on the device.
- Grant the app biometric permission – on Android this is runtime; on iOS it’s prompted at first use.
- Install a debug build that enables verbose logging (
flutter run --verbose). - Prepare a fallback PIN/password in the app settings to verify error paths.
Execution
| Step | Action | Observation |
|---|---|---|
| 1 | Launch the app, navigate to the login screen. | Login UI visible, biometric button enabled. |
| 2 | Tap the biometric button. | System biometric prompt appears (fingerprint icon or Face ID animation). |
| 3 | Present a valid biometric. | Prompt dismisses, app transitions to home screen within 2 s. |
| 4 | Return to login screen (via logout or back). | Biometric button still enabled. |
| 5 | Tap biometric button, then cancel the prompt. | Prompt disappears, login screen shows “Authentication cancelled”, app stays on login. |
| 6 | Repeatedly present an invalid biometric (wrong finger, look away) until lockout threshold is reached. | After configured attempts, prompt shows lockout message, offers device credentials. |
| 7 | Enter correct device PIN/password. | App logs in successfully, confirming fallback works. |
| 8 | Minimize the app, then trigger biometric login via a notification deep‑link. | App restores to foreground, prompt appears, authentication succeeds. |
| 9 | Rotate device while prompt is visible. | Prompt stays centered, no clipping, auth result delivered. |
| 10 | Enable TalkBack (Android) or VoiceOver (iOS). Focus on biometric button. | Audio label reads “Use biometric login, button”. |
| 11 | Switch to high‑contrast mode. Verify button contrast with a color‑contrast analyzer. | Ratio ≥ 4.5:1. |
| 12 | Use adb shell am get-debug-app (Android) or Console.app (iOS) to confirm no biometric raw data appears in logs. | Logs contain only result codes like BiometricResult.success. |
| 13 | (Optional) Root the device or jailbreak, attempt to pull /data/data/ or Library/Preferences. Verify no biometric templates stored. | Only encrypted keys or auth tokens present. |
| 14 | (Optional) Use Frida to inject a script that replays the last successful biometric intent. Observe app reaction. | App rejects replay, shows failure. |
Notes
- Device variance – Test on at least one Android device with fingerprint, one with face unlock (if available), and one iOS device with Touch ID and one with Face ID.
- API level – On Android, test both API 28 (BiometricPrompt introduced) and API 30+ (strong biometrics) to ensure backward compatibility.
- iOS version – Verify behavior on iOS 13 (where
LAContextchanged) and iOS 16+ (whereLocalAuthenticationaddeddeviceOwnerAuthenticatedWithBiometrics).
Manual testing catches subtle UI glitches, accessibility oversights, and device‑specific quirks that automated scripts may miss if they rely solely on mock platform channels.
Automated Testing Strategies for Flutter Biometric Login
Automation speeds regression and CI validation. Because the biometric prompt is native, you must either mock the platform channel or use real device farms that expose biometric simulation.
1. Mock the local_auth Plugin
The most common approach is to provide a fake LocalAuthentication instance via dependency injection. Below is a minimal example using mockito.
// auth_service.dart
abstract class AuthService {
Future<bool> authenticate({
String? localizedReason,
bool useErrorDialogs = true,
bool stickyAuth = false,
});
}
// real_auth_service.dart
import 'package:local_auth/local_auth.dart';
class RealAuthService implements AuthService {
final LocalAuthentication _auth = LocalAuthentication();
@override
Future<bool> authenticate({
String? localizedReason,
bool useErrorDialogs = true,
bool stickyAuth = false,
}) async {
final bool didAuthenticate = await _auth.authenticate(
localizedReason: localizedReason ?? 'Sign in',
useErrorDialogs: useErrorDialogs,
stickyAuth: stickyAuth,
);
return didAuthenticate;
}
}
// fake_auth_service.dart
import 'package:mockito/mockito.dart';
class FakeAuthService extends Mock implements AuthService {}
In your test, you configure the fake to return specific results:
// biometric_login_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
void main() {
late FakeAuthService fakeAuth;
setUp(() {
fakeAuth = FakeAuthService();
});
testWidgets('successful fingerprint login navigates to home', (tester) async {
when(fakeAuth.authenticate(any)).thenAnswer((_) async => true);
await tester.pumpWidget(
Provider<AuthService>.value(
value: fakeAuth,
child: const LoginPage(),
),
);
await tester.tap(find.byIcon(Icons.fingerprint));
await tester.pumpAndSettle();
expect(find.text('Welcome'), findsOneWidget);
verify(fakeAuth.authenticate(any)).called(1);
});
testWidgets('user cancels prompt shows error', (tester) async {
when(fakeAuth.authenticate(any)).thenAnswer((_) async => false);
await tester.pumpWidget(
Provider<AuthService>.value(
value: fakeAuth,
child: const LoginPage(),
),
);
await tester.tap(find.byIcon(Icons.fingerprint));
await tester.pump();
expect(find.text('Authentication cancelled'), findsOneWidget);
});
}
Pros: Fast, deterministic, runs on any CI agent.
Cons: Does not validate native plugin integration, UI thread behavior, or permission handling.
2. Use the local_auth Plugin’s Built‑in Test Mode (Android Only)
Starting with version 0.7.0, the plugin exposes a setMockLocalAuthentication method for instrumentation tests. This lets you drive the real plugin but feed it simulated biometric results.
Add to android/app/src/androidTest/java/com/example/app/MainActivityFlutterTest.java:
@Rule
public FlutterActivityTestRule<?> rule = new FlutterActivityTestRule<>(MainActivity.class);
@Test
public void biometricSuccessTest() {
// Enable mock mode
LocalAuthenticationPlugin.setMockLocalAuthentication(true);
// Simulate a successful fingerprint
LocalAuthenticationPlugin.setMockLocalAuthenticationResult(true);
// Launch the app
rule.launchActivity(null);
// Tap biometric button
onView(withId(R.id.biometric_button)).perform(click());
// Wait for home screen
onView(withText("Welcome")).check(matches(isDisplayed()));
}
Pros: Tests real plugin code path, respects threading, and surface‑level UI.
Cons: Requires Android instrumentation tests; iOS lacks an equivalent mock mechanism.
3. Real Device Farm with Biometric Simulation
Services like Firebase Test Lab, AWS Device Farm, or Sauce Labs let you upload an APK/IPA and run scripts that interact with the actual biometric HAL via adb commands:
- Android:
adb shell cmd uimatest fingerprintsimulates a fingerprint press. - iOS: Use
xcrun simctl biometry enrollandxcrun simctl biometry matchon simulators (Xcode 12.4+). Real devices require a physical finger or face, but you can automate with a robotic arm in a lab.
Example Bash snippet for Firebase Test Lab:
gcloud firebase test android run \
--type instrumentation \
--app app-debug.apk \
--test biometric_test.apk \
--device model=Pixel3,version=30,locale=en,orientation=portrait \
--environment-variables fingerprintId=1
Inside the test:
@Before
public void enrollFingerprint() {
// Enroll a fake fingerprint id 1 for the test session
DeviceUtils.enrollFingerprint(1);
}
@Test
public void testBiometricLogin() {
onView(withId(R.id.biometric_button)).perform(click());
// Simulate finger press
DeviceUtils.triggerFingerprint(1);
onView(withText("Welcome")).check(matches(isDisplayed()));
}
Pros: Closest to production reality; catches native crashes, ANRs, and permission flows.
Cons: Slower, higher cost, requires device‑farm access.
4. Hybrid Approach: Unit + Integration
A practical pipeline:
- Unit tests with mocked
AuthServicefor business logic (state transitions, error handling). - Widget tests that inject a
FakeAuthServiceto verify UI reacts correctly to success/failure/cancel. - Instrumentation tests (Android) or UI tests (XCTest) on a device farm using the real plugin with mock biometric simulation to validate native integration.
- Periodic manual exploratory runs on a matrix of physical devices to catch device‑specific regressions.
This layered strategy gives fast feedback while retaining confidence in the native layering Tooling and Libraries for Biometric Testing in Flutter
| Tool/Language | Notes |
|---|
- Speed for PR validation (unit + widget).
- Coverage for native integration (instrumentation).
- Realism for release validation (device farm or manual).
Autonomous, Persona‑Driven Exploration with SUSA
While scripted tests verify expected paths, autonomous exploration can surface unexpected states—especially when biometric flows intersect with app navigation, deep links, or background processes. SUSA (SUSATest) is an autonomous QA platform that explores an uploaded APK or web URL using a variety of user personas, each with distinct behavior patterns, and reports crashes, ANRs, dead buttons, accessibility issues, and UX friction.
How SUSA Approaches Biometric Login
- Persona‑Based Interaction
- Novice – taps the biometric button repeatedly, waits long intervals, reads prompts carefully.
- Impatient – double‑taps, cancels quickly, tries fallback PIN immediately.
- Power User – uses biometric, then immediately attempts a sensitive transaction (e.g., purchase) to test session continuity.
- Accessibility – enables TalkBack/VoiceOver, navigates via swipe gestures, checks label announcements.
- Adversarial – attempts to replay biometric intents, injects malformed platform channel messages, triggers biometric prompt while app is in background or locked.
- Exploration Mechanics
SUSA instruments the Flutter app via method‑channel hooks, records every UI state (route, widget tree, accessibility labels), and builds a transition graph. When it encounters the biometric login screen, it:
- Sends the platform channel request to invoke
LocalAuthentication.authenticate. - Waits for the native prompt (detected via overlay detection).
- Simulates biometric success/failure/cancel using the platform’s built‑in test hooks (Android) or accessibility gestures (iOS).
- Continues exploration from the resulting state (home screen, error screen, lockout screen).
- Bug Detection Specific to Biometrics
- Dead button after lockout – SUSA noticed that after five failed attempts, the biometric button stayed enabled but clicking it did nothing because the plugin returned a
BiometricError.lockedOutthat the UI ignored. - ANR on rotation – When the device orientation changed while the prompt was visible, the main thread blocked waiting for the platform channel response, causing a 5‑second ANR flagged by SUSA’s performance monitor.
- Accessibility label missing – The accessibility persona reported that the biometric button announced only “Button” instead of “Use biometric login, button”, leading to a WCAG failure logged by SUSA’s axe‑core integration.
- Security leakage – The adversarial persona used Frida to hook the
LocalAuthenticationplatform method and extract the nonce used for key generation; SUSA flagged the presence of raw nonce in logs as a privacy issue.
Integrating SUSA into Your CI
- Upload APK/AAB –
susatest-agent upload app-release.apk. - Define persona set – use the default set or create a custom JSON focusing on biometric scenarios (e.g., add “biometric‑power‑user” that attempts a purchase right after auth).
- Run exploration –
susatest-agent run --personas novice,impatient,accessibility,adversarial --duration 15m. - Retrieve report – the agent returns a JSON with
crashes,ANRs,accessibilityViolations, andflowResults. Each flow includes a PASS/FAIL verdict for login, signup, checkout, etc. - Gate on failures – fail the build if any
flowResultfor the biometric login path is FAILED or if newaccessibilityViolationsappear.
Because SUSA learns from each run, repeated executions explore deeper paths (e.g., logging in, then navigating to settings to change biometric preference, then logging out) without you writing additional test code.
Checklist for Biometric Login Testing in Flutter
Use this concise list before every release. Mark each item as Done or Blocked.
| Area | Item | How to Verify |
|---|---|---|
| Happy Path | Fingerprint login succeeds and navigates to target screen | Manual tap + valid biometric; automated widget test with mock success |
| Face ID login succeeds (iOS) | Same as above on iOS device | |
| Error Paths | Biometric unavailable shows fallback | Disable biometric in settings; tap button; verify fallback prompt |
| User cancel handled gracefully | Tap button then cancel; verify “Authentication cancelled” toast | |
| Lockout after N failures triggers fallback | Simulate failures (wrong finger/face) until lockout; verify fallback offered | |
| Background invocation works | Minimize app, trigger biometric via notification deep‑link; verify prompt appears | |
| Orientation change during prompt does not break UI | Start auth, rotate device, verify prompt stays centered and auth completes | |
| Accessibility | Button has descriptive label | Enable TalkBack/VoiceOver; verify announcement |
| Contrast meets WCAG AA | Use contrast checker on button in normal and high‑contrast themes | |
| Touch target ≥ 48 dp | Inspect layout or use UI Automator to measure hit‑area | |
| Security | No raw biometric data stored | Inspect app private storage after auth; confirm only encrypted keys/tokens |
| Key invalidated on biometric change | Enroll new fingerprint, attempt to use old token; verify rejection | |
| Resistant to replay attack | Use Frida/adb to capture and replay auth intent; verify failure | |
| Privacy | Permission rationale displayed | First‑launch attempt; verify system dialog shows custom rationale |
| No biometric data in logs | Run with verbose logging; grep for fingerprint/face terms; ensure none appear | |
| Performance | Authentication completes < 2 s on median device | Measure with Stopwatch in test or SUSA performance metrics |
| No ANR during prompt | Monitor CPU thread traces; ensure main thread not blocked > 5 s | |
| Regression | Widget test suite passes | Run flutter test on widget and unit tests |
| Instrumentation test suite passes (Android) | Run ./gradlew connectedAndroidTest | |
| Device‑farm smoke test passes | Run a short Firebase Test Lab or AWS Device Farm job | |
| SUSA exploration returns PASS for login flow | Run susatest-agent with biometric‑focused personas; check flowResult |
If any item is Blocked, investigate and fix before promoting the build to staging or production.
Takeaways and Next Steps
Biometric login is a high‑impact feature that blends platform‑specific security with Flutter’s cross‑platform UI. Testing it demands a layered strategy:
- Start with unit and widget tests using a mocked
AuthServiceto validate state machines and UI reactions at lightning speed. - Add instrumentation or device‑farm tests that exercise the real
local_authplugin, confirming threading, permission handling, and native error codes. - Supplement with autonomous, persona‑driven exploration (e.g., via SUSA) to catch edge cases that only manifest when real users—especially impatient, impaired, or adversarial ones—interact with the app in unpredictable ways.
- Maintain a living test matrix that covers happy paths, error conditions, accessibility, security, and privacy, and evolve it as you discover device‑specific quirks.
- Automate regression gates in CI so any breakage in the biometric flow fails the build before it reaches users.
By following the guide above, you will ship a biometric login experience that is fast, reliable, accessible, and resilient to the kinds of bugs that slip through scripted tests alone. 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