23、单元测试:Mock BiometricPrompt、测试认证回调、测试错误处理
单元测试这件事,说实话,很多做生物识别的同学容易忽略。我早期做虹膜项目时也踩过这个坑——代码写完了,真机一跑,认证回调死活不触发,排查了半天才发现是 mock 没写好。嗯,今天我们就来把这部分彻底讲清楚。
为什么要 Mock BiometricPrompt?
BiometricPrompt 是 Android 系统提供的类,它依赖硬件和系统服务。在单元测试环境里,你不可能真的去扫描虹膜。所以我们需要 mock 掉它,模拟出「认证成功」「认证失败」「发生错误」这三种场景。
说白了,我们要让测试代码以为自己在跟真正的 BiometricPrompt 交互,但实际上我们在控制它的行为。
搭建测试环境
首先,在 build.gradle 里加上必要的依赖。我个人习惯用 Mockito 配合 AndroidX Test:
testImplementation 'org.mockito:mockito-core:4.11.0'
testImplementation 'androidx.test:core:1.5.0'
testImplementation 'androidx.biometric:biometric:1.2.0-alpha05'
注意,biometric 库本身也要加进来,因为我们需要引用 BiometricPrompt 的类型定义。
Mock BiometricPrompt 实例
这里有个关键点:BiometricPrompt 的构造函数需要 Activity 和 Executor。我们不能直接 new 一个出来,得用 Mockito 去 mock:
@Mock
private BiometricPrompt mockBiometricPrompt;
@Mock
private BiometricPrompt.AuthenticationCallback mockCallback;
@Before
public void setUp() {
MockitoAnnotations.openMocks(this);
// 模拟认证回调的各个方法
doAnswer(invocation -> {
BiometricPrompt.AuthenticationResult result = invocation.getArgument(0);
// 这里可以自定义 result 的内容
return null;
}).when(mockCallback).onAuthenticationSucceeded(any());
}
我曾经在项目里犯过一个低级错误——忘记调用 MockitoAnnotations.openMocks(this),结果跑测试时一直报 NullPointerException。排查了半小时才发现是注解没生效。你想想看,这种低级错误多耽误时间。
测试认证成功回调
当用户虹膜匹配成功时,系统会调用 onAuthenticationSucceeded。我们要验证自己的业务逻辑是否正确响应:
@Test
public void testAuthenticationSuccess() {
// 模拟认证成功
BiometricPrompt.AuthenticationResult result = mock(BiometricPrompt.AuthenticationResult.class);
mockCallback.onAuthenticationSucceeded(result);
// 验证我们的 ViewModel 或 Presenter 收到了成功事件
verify(viewModel).onBiometricSuccess();
// 验证 UI 状态更新
assertEquals(ViewState.SUCCESS, uiState.getValue());
}
这里要注意,AuthenticationResult 里的 CryptoObject 可能为 null。如果你的业务逻辑依赖它,记得在 mock 时设置好。
测试认证失败回调
认证失败对应 onAuthenticationFailed。这个回调没有参数,但我们需要确保 UI 给出正确提示:
@Test
public void testAuthenticationFailed() {
mockCallback.onAuthenticationFailed();
verify(viewModel).onBiometricFailed();
// 检查错误提示是否展示
assertTrue(errorMessage.isVisible());
assertEquals("虹膜识别失败,请重试", errorMessage.getText());
}
嗯,这里有个小细节:onAuthenticationFailed 和 onAuthenticationError 是两回事。前者只是匹配不成功,用户可以立即重试;后者是系统出问题了,需要等待一段时间。我在项目中见过不少同学把这两个混为一谈,导致用户体验很差。
测试错误处理
错误处理是最容易出 bug 的地方。系统会通过 onAuthenticationError 返回错误码和错误信息。常见的错误码有:
| 错误码 | 含义 | 处理方式 |
|---|---|---|
| BIOMETRIC_ERROR_HW_UNAVAILABLE | 硬件不可用 | 提示用户稍后再试 |
| BIOMETRIC_ERROR_NO_BIOMETRICS | 未录入虹膜 | 引导用户去设置 |
| BIOMETRIC_ERROR_LOCKOUT | 尝试次数过多被锁定 | 等待一段时间或使用密码 |
| BIOMETRIC_ERROR_USER_CANCELED | 用户主动取消 | 不做特殊处理 |
测试代码可以这样写:
@Test
public void testHardwareUnavailableError() {
int errorCode = BiometricPrompt.BIOMETRIC_ERROR_HW_UNAVAILABLE;
String errorMsg = "虹膜硬件暂时不可用";
mockCallback.onAuthenticationError(errorCode, errorMsg);
verify(viewModel).onBiometricError(errorCode, errorMsg);
// 验证弹窗提示
assertEquals("硬件不可用,请稍后重试", dialogMessage.getValue());
}
@Test
public void testLockoutError() {
int errorCode = BiometricPrompt.BIOMETRIC_ERROR_LOCKOUT;
String errorMsg = "尝试次数过多";
mockCallback.onAuthenticationError(errorCode, errorMsg);
verify(viewModel).onBiometricError(errorCode, errorMsg);
// 验证是否显示倒计时或密码选项
assertTrue(showPasswordOption.isVisible());
}
用 SVG 梳理测试流程
下面这张图展示了单元测试的核心逻辑:
一些实用技巧
- 使用 ArgumentCaptor: 当回调参数比较复杂时,用 ArgumentCaptor 捕获参数再断言,比直接 verify 更灵活。
- 测试边界情况: 比如错误码为 0 的情况、错误信息为 null 的情况。我遇到过厂商返回空字符串的错误信息,导致 app 崩溃。
- 不要 mock 所有东西: 你的业务逻辑类(比如 ViewModel)应该用真实实例,只 mock 系统 API 和外部依赖。
单元测试写好了,后续的回归测试会轻松很多。每次修改代码后跑一遍测试,就能快速知道有没有破坏已有的认证逻辑。嗯,这部分内容就到这里,希望对你有所帮助。