19、单元测试与UI测试:JUnit测试BiometricPrompt、Espresso测试认证流程、Mockito模拟BiometricManager
说实话,很多开发者写完生物识别代码就觉得完事了。但我在项目里吃过亏——有一次上线前才发现,某款低端机型上BiometricPrompt压根没回调。从那以后,我养成了一个习惯:测试不写全,代码不上线。
这一章,我们就来聊聊怎么给生物识别模块写测试。我会从三个角度切入:单元测试、UI测试、模拟依赖。说白了,就是让你在没真机、没指纹、没人脸的情况下,也能把代码测个底朝天。
为什么需要测试生物识别代码?
你想想看,生物识别涉及硬件、系统服务、用户交互。任何一个环节出问题,用户都会直接感知到——弹窗不出现、认证失败没提示、甚至直接闪退。我见过一个案例:某App在Android 11上人脸识别正常,到了Android 12就因为BiometricPrompt的构造函数变了,直接崩溃。这种问题,靠手工测试很难覆盖全。
所以,我们需要三套测试方案:
- JUnit:测试BiometricPrompt的回调逻辑
- Mockito:模拟BiometricManager,绕过真实硬件
- Espresso:模拟用户点击、验证UI状态变化
核心思路:把生物识别的「硬件依赖」和「业务逻辑」解耦。硬件部分用Mockito模拟,业务逻辑用JUnit验证,UI交互用Espresso覆盖。
1. 用Mockito模拟BiometricManager
先解决最头疼的问题:没有真机怎么测?BiometricManager需要跟硬件通信,单元测试里根本拿不到。我的做法是——用Mockito把它整个替换掉。
// 模拟BiometricManager
@RunWith(MockitoJUnitRunner.class)
public class BiometricManagerTest {
@Mock
private BiometricManager mockManager;
@Test
public void testCanAuthenticate_whenHardwareAvailable() {
// 模拟硬件可用
when(mockManager.canAuthenticate()).thenReturn(
BiometricManager.BIOMETRIC_SUCCESS
);
int result = mockManager.canAuthenticate();
assertEquals(BiometricManager.BIOMETRIC_SUCCESS, result);
}
@Test
public void testCanAuthenticate_whenNoHardware() {
// 模拟无硬件
when(mockManager.canAuthenticate()).thenReturn(
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE
);
int result = mockManager.canAuthenticate();
assertEquals(BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE, result);
}
}
我的习惯:我会把BiometricManager封装成一个接口,比如叫BiometricChecker。这样Mockito只需要模拟接口,不用跟Android框架类硬刚。你想想看,如果直接模拟BiometricManager,万一Google改了构造函数,你的测试代码也得跟着改——划不来。
2. JUnit测试BiometricPrompt回调
BiometricPrompt的回调是异步的,但JUnit是同步的。怎么测?用CountDownLatch。这是我踩过坑之后才学会的——一开始我直接调callback,结果发现onError和onSuccess永远不同时触发,测试结果时好时坏。
public class BiometricCallbackTest {
private BiometricPrompt.AuthenticationCallback callback;
private CountDownLatch latch;
@Before
public void setUp() {
latch = new CountDownLatch(1);
callback = new BiometricPrompt.AuthenticationCallback() {
@Override
public void onAuthenticationSucceeded(
BiometricPrompt.AuthenticationResult result) {
// 标记成功
latch.countDown();
}
@Override
public void onAuthenticationError(int errorCode,
CharSequence errString) {
// 标记失败
latch.countDown();
}
};
}
@Test
public void testOnSuccess_calledOnce() throws Exception {
// 模拟认证成功
callback.onAuthenticationSucceeded(
new BiometricPrompt.AuthenticationResult(null, 0)
);
// 等待回调执行
boolean called = latch.await(2, TimeUnit.SECONDS);
assertTrue("回调未触发", called);
}
@Test
public void testOnError_calledWithCorrectCode() throws Exception {
callback.onAuthenticationError(
BiometricPrompt.BIOMETRIC_ERROR_LOCKOUT,
"Too many attempts"
);
boolean called = latch.await(2, TimeUnit.SECONDS);
assertTrue(called);
}
}
我曾经踩过的坑:CountDownLatch的等待时间设得太短。有一次我设了500毫秒,结果在低端机型上回调慢了100毫秒,测试就挂了。后来我统一改成2秒,再也没出过问题。记住:测试环境比真机慢,给足余量。
3. Espresso测试认证流程
UI测试要模拟的是:用户点击按钮 → 弹出生物识别对话框 → 认证成功/失败 → UI更新。Espresso配合ActivityTestRule,可以做到这一点。
@RunWith(AndroidJUnit4.class)
public class BiometricUiTest {
@Rule
public ActivityTestRule<MainActivity> activityRule =
new ActivityTestRule<>(MainActivity.class);
@Test
public void testAuthenticateButton_showsSuccess() {
// 点击「开始认证」按钮
onView(withId(R.id.btn_authenticate))
.perform(click());
// 模拟系统级生物识别弹窗
// 注意:这里需要依赖Espresso的system alert窗口支持
onView(withText("认证成功"))
.check(matches(isDisplayed()));
}
@Test
public void testAuthenticateButton_showsError() {
onView(withId(R.id.btn_authenticate))
.perform(click());
// 模拟认证失败
onView(withText("认证失败"))
.check(matches(isDisplayed()));
}
}
关键点:Espresso测试生物识别时,最大的难点是系统弹窗。BiometricPrompt弹出的对话框是系统级的,Espresso默认只能测App内的View。我的解决方案是:在测试环境下,用一个自定义的DialogFragment代替系统弹窗,这样Espresso就能直接定位到UI元素了。
4. 组合测试:Mockito + JUnit + Espresso
实际项目中,我经常把三者组合起来用。比如这样:
@RunWith(AndroidJUnit4.class)
public class BiometricIntegrationTest {
@Mock
private BiometricManager mockManager;
@Rule
public MockitoRule mockitoRule = MockitoJUnit.rule();
@Rule
public ActivityTestRule<MainActivity> activityRule =
new ActivityTestRule<>(MainActivity.class, true, false);
@Test
public void testFullFlow_hardwareAvailable_authSuccess() {
// 1. 模拟硬件可用
when(mockManager.canAuthenticate())
.thenReturn(BiometricManager.BIOMETRIC_SUCCESS);
// 2. 启动Activity
activityRule.launchActivity(new Intent());
// 3. 点击认证按钮
onView(withId(R.id.btn_authenticate)).perform(click());
// 4. 验证UI状态
onView(withId(R.id.tv_status))
.check(matches(withText("请进行指纹或人脸认证")));
}
}
我的建议:别一上来就写集成测试。先写Mockito的单元测试,把BiometricManager的各种返回值都覆盖到。然后再写JUnit的回调测试。最后才用Espresso做UI验证。这样出了问题,你能快速定位是硬件模拟层、回调逻辑层、还是UI层的问题。
5. 测试覆盖率清单
我整理了一个清单,每次写生物识别测试时都会对照检查:
| 测试场景 | 测试类型 | 关键验证点 |
|---|---|---|
| 硬件可用 | Mockito | canAuthenticate() 返回 BIOMETRIC_SUCCESS |
| 无硬件 | Mockito | 返回 BIOMETRIC_ERROR_NO_HARDWARE |
| 认证成功 | JUnit | onAuthenticationSucceeded 被调用 |
| 认证失败 | JUnit | onAuthenticationError 被调用,errorCode 正确 |
| 用户取消 | JUnit | errorCode = BIOMETRIC_ERROR_USER_CANCELED |
| 多次锁定 | JUnit | errorCode = BIOMETRIC_ERROR_LOCKOUT |
| UI 显示成功 | Espresso | TextView 显示「认证成功」 |
| UI 显示失败 | Espresso | TextView 显示「认证失败」 |
| 按钮状态 | Espresso | 认证中按钮不可点击 |
嗯,这张表基本覆盖了我在生产环境中遇到过的所有情况。你写测试的时候,可以照着这个清单一个一个过。
写在最后
测试生物识别代码,说白了就是把不确定性变成确定性。硬件不可控?用Mockito模拟。回调异步?用CountDownLatch同步。UI难定位?用自定义DialogFragment替代系统弹窗。
我个人习惯是:每个新功能上线前,至少跑一遍这个清单里的所有测试。虽然写测试花时间,但比起用户反馈「人脸识别用不了」然后紧急发版,这点时间花得值。