•
Drizz raises $2.7M in seed funding •
•
Featured on Forbes
•
Drizz raises $2.7M in seed funding •
•
Featured on Forbes

Key takeaways
Biometric authentication testing on mobile is the practice of verifying that your app handles biometric prompts, successes, failures, fallbacks, key invalidations, and lockout states correctly on both iOS and Android. Most teams test happy path (user opens app, Face ID succeeds, user is in) and stop. That's testing 15% of surface. The other 85% is where security bugs and one-star reviews come from.
Getting this right matters because biometric authentication is now primary login path for banking apps, payment apps, health apps, and increasingly for consumer social apps. A biometric bug isn't a login inconvenience. It's a locked-out user or, worse, an incorrectly authenticated one.
Before you test it, you need to know what it is. Every generic article skips this and jumps straight to "Test Face ID and fingerprint." You can't meaningfully test what you don't understand.
Biometric authentication on mobile is a hardware-backed identity check. The user's biometric data (fingerprint template, face embedding, and iris scan) is stored in a dedicated secure hardware component: Secure Enclave on iOS, StrongBox or Trusted Execution Environment (TEE) on Android. Your app never sees biometric data. Your app calls an API, OS handles biometric check with hardware, and OS tells your app "authenticated" or "not authenticated".
There's a second layer that matters for security-critical apps. Cryptographic keys can be created that are only usable after biometric authentication. When you decrypt something with a biometric-protected key, OS enforces a biometric check as part of the crypto operation. This is different from a boolean "did user auth" check because a key literally can't work without a valid biometric. That's difference between authentication (proving identity) and authorisation (allowing an operation).
Testing biometric authentication means testing both layers: "was user authenticated" flow and the "does app correctly use biometric-protected keys" flow. Most teams test first. Almost none test second.
Once password login works, biometric authentication testing is natural next layer of your auth test coverage. It doesn't replace password testing. It adds to it, because biometric flows always have a password fallback somewhere.
On iOS you're calling LocalAuthentication framework. Most of it goes through LAContext.
The basic flow:
The LAError codes you need to test against are documented in Apple's LocalAuthentication error reference. Face ID and Touch ID coverage falls under biometric authentication testing on iOS, and Touch ID specifically is now legacy testing (only iPhone SE and older iPads still have it), but you need it if you claim to support those devices. The LAError codes you actually hit in production:
You should have a test case for every one of these. Not "we handle failures gracefully." A test that specifically triggers each error and verifies correct fallback UI appears.
Then there's Keychain access control. If your app stores anything in Keychain protected by biometrics (session tokens, encryption keys), you're using SecAccessControlCreateWithFlags with flags like .userPresence, .biometryAny, or .biometryCurrentSet. That last one is interesting one: .biometryCurrentSet invalidates stored item if user changes their enrolled biometrics. A user enrolls a new fingerprint, and your Keychain item is now unreadable. Test that. Your app should handle it by re-authenticating and re-storing item, not crashing or showing a broken state.
Android's story is messier because it evolved. FingerprintManager was deprecated. BiometricPrompt is current recommended API. BiometricManager gives you capability checks. Underneath, hardware-backed keys use AndroidKeyStore with either StrongBox or TEE.
The authenticator classes matter. When you build a BiometricPrompt.PromptInfo, you specify setAllowedAuthenticators(...) with combinations of:
Testing this correctly means verifying that if your app requires BIOMETRIC_STRONG for a payment authorization, it doesn't accept BIOMETRIC_WEAK because user's phone only has face unlock (which is weak on most Android devices). Google's device compatibility varies wildly here. A Pixel 8's face unlock is Class 3. A Samsung Galaxy A54's face unlock is Class 2. Your app needs to know difference and behave correctly.
The error codes in BiometricPrompt.AuthenticationCallback:
Same drill as iOS. Every one of these needs a test case that specifically triggers it and verifies app response. Google's official BiometricPrompt documentation has full sample code and callback signatures if you're wiring this up from scratch.
The key invalidation problem also exists on Android. When you create a KeyStore key with setUserAuthenticationRequired(true) and setInvalidatedByBiometricEnrollment(true), key becomes unusable if user adds or removes a biometric. Same failure mode as iOS .biometryCurrentSet. Same testing requirement.
Biometric authentication testing sits inside a broader security testing scope, and how you handle these hardware-backed key invalidations is what auditors will actually look at during a security review.
Generic articles list "test success, test failure, test cancel." That's three tests. Here's actual list of states you need to cover.
Setup states:
Authentication attempt states:
Environmental states:
Enrollment change states:
Cross-platform states:
That's 21 test cases minimum. Not one. Not three. Twenty-one. And that's before you multiply by device matrix.
Every article that recommends running biometric tests on simulators is misleading you.
Xcode Simulator lets you toggle Face ID / Touch ID behavior via Features menu: Enrolled, Matching Face, Non-matching Face. That's useful for basic flow testing. But Simulator doesn't actually run Secure Enclave. Your keys aren't hardware-backed. Your .biometryCurrentSet flag doesn't behave same as on a real device because there's no real biometric enrollment to compare against. Attestation calls don't work.
Android emulator has similar limitations. You can go to Settings → Security → Biometrics and set up a fingerprint, then use adb -e emu finger touch <finger_id> to simulate a touch. That triggers callback. But hasStrongBox() returns false in most emulator configurations because there's no StrongBox hardware. Class 3 vs Class 2 behavior isn't reliably simulated.
For anything that involves cryptographic operations, attestation, or real distinction between BIOMETRIC_STRONG and BIOMETRIC_WEAK, you need real devices. This is a hard requirement for security-critical apps: banking, payments, health, government.
The practical implication for your test strategy: keep emulator/simulator tests for happy path and basic error handling, but require real devices for security-critical scenarios and enrollment change tests. This split roughly matches "unit and component tests on emulators, integration and E2E on real devices."
This is section I want you to actually take away.
The key invalidation flow is: user enrolls a new biometric (adds a fingerprint, changes their Face ID) → OS invalidates all biometric-protected keys that were created with invalidatedByBiometricEnrollment flag (Android) or .biometryCurrentSet access control (iOS) → your app tries to use those keys → they fail → your app needs to handle this and re-register key.
Most apps don't test this. Most apps also don't handle it correctly. The failure modes we see in production:
The correct behavior is: detect key invalidation, tell user "we noticed you updated your biometric settings, please authenticate with your password to re-enable biometric login," verify password, re-create biometric key, log user in. That's 4 steps. Nobody tests all 4.
To test this on a real iOS device:
On Android: same idea, but you add a fingerprint via Settings → Security → Fingerprint, then verify.
This test is manual today for most teams because framework support for automating "add a new biometric to OS" is essentially nonexistent. You can't script this through Appium. You do it by hand once per release on primary devices in your matrix. Ten minutes of real testing catches bugs that would otherwise ship.
Both platforms lock out biometric authentication after too many failed attempts. iOS locks after 5 failed Face ID attempts, then requires passcode to unlock biometric. Android has similar behavior with ERROR_LOCKOUT (temporary, unlocks after 30 seconds) and ERROR_LOCKOUT_PERMANENT (needs credential to unlock).
Your app should:
Testing this requires actually failing biometric N times in a row. On a real iPhone, that means presenting your face at an angle that Face ID won't match, 5 times. Or covering sensor. Or entering someone else's face. This is genuinely tedious manual testing but you should do it once per major release.
For general app testing, selector-based tools (Appium, XCUITest, Espresso) are already fragile. For biometric testing, they mostly can't do job at all.
The biometric prompt is a system dialog rendered by OS, not by your app. Its accessibility hierarchy is limited, its buttons don't have consistent IDs across OS versions, and simulating a biometric event requires either a virtual sensor (emulator/simulator only) or a physical action (real device, no automation). Appium supports biometric enrollment simulation on virtual devices only. On real devices, you're back to manual testing.
BrowserStack, Sauce Labs, and TestGrid all offer platform-specific biometric simulation features that hook into their real device infrastructure. Those work but tie you to their platform, and they don't fully test hardware-backed key flows because their simulation is at OS API level, not actual Secure Enclave / StrongBox.
The upshot: automated biometric testing is limited. Manual testing is honest gap-filler for flows that matter most.
The biometric prompt itself might not be automatable end-to-end. Everything around it (flow that leads up to it, fallback flow after cancel, error message after failure, state after key invalidation) can and should be automated.
Drizz tests are written as plain English commands. Tap "Log in with Face ID". Validate "Face ID is locked. Enter password" is visible. Vision AI reads screen way a person would, so when your app shows an error state after biometric failure, test verifies correct message renders regardless of which OS version renders underlying system dialog.
Concrete before-and-after on "biometric failure fallback to password" test flow.
Before, on Appium: Test triggers biometric prompt (works on simulator only, using Appium's mobile: sendBiometricMatch command). Simulates a failed match. Expects to find a fallback button by accessibility ID. That ID differs between iOS 15 and iOS 17. QA lead maintains two selectors and a version check. Meanwhile underlying UI has changed twice this year for accessibility reasons. Test is red on most CI runs. Nobody trusts what it's actually catching.
After, on Drizz: Same test written in plain English. Vision AI sees failure message on screen ("Face ID not recognized. Try again or use password") and taps "Use password" regardless of underlying accessibility ID. When OS updates change wording slightly to "Face ID didn't match. Enter password," test can be updated with new visible text in one line. No accessibility ID chase. No per-OS-version selector maintenance.
For biometric flows specifically, this matters because surrounding UI (before prompt, after prompt, error messages, fallback screens) is where you spend most of your testing energy. The biometric prompt itself is a small piece of flow. The recovery paths around it are where user experience lives or dies.
Verifying that an app correctly handles biometric prompts, successes, failures, fallbacks, key invalidations, and lockouts across iOS and Android.
Partially. Emulators handle happy path and errors but not hardware attestation or true key invalidation properly.
Poor handling of key invalidation after enrollment change, missing fallback UI on lockout, and silent failures on disable.
Yes for security-critical flows. Emulators cover happy paths. Real devices needed for cryptographic keys and enrollment changes.
BIOMETRIC_STRONG (Class 3) is hardware-backed and can unlock cryptographic keys. BIOMETRIC_WEAK (Class 2) is lower assurance and cannot.
Not fully. The biometric prompt is a system dialog. Everything around it can be automated. The prompt needs manual verification.