24、单元测试与集成测试:使用Mockito模拟SensorManager、编写传感器相关测试用例
说实话,很多做Android开发的朋友,一听到「测试」两个字就头大。尤其是传感器这块,硬件不在手里,模拟器又不靠谱,怎么测?
我早年刚接手一个距离传感器项目时,也踩过这个坑。代码写完了,跑真机一测,发现逻辑全乱套。后来我学乖了——先把测试写好,再调硬件。今天咱们就聊聊,怎么用Mockito把SensorManager给「骗」过去,写出靠谱的传感器测试用例。
为什么需要Mock SensorManager?
你想想看,距离传感器是硬件设备。你的测试代码跑在开发机上,根本没有真实的传感器数据。如果直接new一个SensorManager,那测试一运行就报空指针,或者压根拿不到数据。
说白了,我们需要一个「替身」。这个替身能假装自己是SensorManager,你想让它返回什么数据,它就返回什么数据。Mockito就是干这个的。
搭建测试环境
首先,在build.gradle里加上依赖。我个人习惯用Mockito + JUnit4的组合,轻量又够用。
dependencies {
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.mockito:mockito-core:4.6.1'
testImplementation 'org.mockito:mockito-inline:4.6.1'
}
注意这个mockito-inline,它允许你mock final类和final方法。Android的SensorManager里有些方法是final的,不加这个会报错。嗯,这里要注意。
模拟SensorManager的基本操作
我们先写一个最简单的测试:验证你的Activity是否正确地注册了距离传感器监听器。
@RunWith(MockitoJUnitRunner.class)
public class DistanceSensorActivityTest {
@Mock
private SensorManager mockSensorManager;
@Mock
private Sensor mockDistanceSensor;
private DistanceSensorActivity activity;
@Before
public void setUp() {
// 让mockSensorManager.getDefaultSensor返回我们的mock传感器
when(mockSensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY))
.thenReturn(mockDistanceSensor);
activity = new DistanceSensorActivity(mockSensorManager);
}
@Test
public void testRegisterListener() {
activity.registerSensor();
// 验证registerListener被调用了一次
verify(mockSensorManager, times(1))
.registerListener(
any(SensorEventListener.class),
eq(mockDistanceSensor),
eq(SensorManager.SENSOR_DELAY_NORMAL)
);
}
}
这段代码里,我做了三件事:
- 用
@Mock创建了两个替身对象 - 用
when().thenReturn()设定了getDefaultSensor的返回值 - 用
verify()检查registerListener是否被正确调用
我在项目中遇到过一个问题:忘了mock getDefaultSensor的返回值,结果测试一直报空指针。排查了半天才发现,mock对象默认返回null。所以,所有被测试代码调用的方法,你都得给它设定好返回值。
模拟传感器事件回调
注册监听器只是第一步。真正要测的,是传感器数据来了之后,你的业务逻辑怎么处理。比如,距离传感器检测到物体靠近,屏幕应该熄灭。
这时候,我们需要手动触发onSensorChanged事件。
@Test
public void testOnSensorChanged_proximityNear() {
// 创建一个传感器事件
SensorEvent mockEvent = mock(SensorEvent.class);
mockEvent.values = new float[]{0.0f}; // 距离为0,表示物体靠近
// 获取注册的监听器
ArgumentCaptor<SensorEventListener> captor =
ArgumentCaptor.forClass(SensorEventListener.class);
verify(mockSensorManager).registerListener(captor.capture(), any(), anyInt());
SensorEventListener listener = captor.getValue();
// 手动触发回调
listener.onSensorChanged(mockEvent);
// 验证屏幕是否熄灭
// 假设你的Activity里有一个方法叫dimScreen()
// 你可以在这里验证dimScreen是否被调用
// 或者验证某个状态值是否改变
assertTrue(activity.isScreenDimmed());
}
这里有个关键点:用ArgumentCaptor捕获注册的监听器对象。因为监听器是在registerListener时传进去的,我们没法直接拿到它。通过captor,我们就能「抓住」这个监听器,然后手动调用它的onSensorChanged方法。
测试多种传感器数据场景
距离传感器不是只有「靠近」和「远离」两种状态。有些设备会返回不同的数值范围,比如0~5cm。你需要覆盖各种边界情况。
| 测试场景 | 模拟数据 | 预期行为 |
|---|---|---|
| 物体靠近(距离=0) | values[0] = 0.0f | 屏幕熄灭 |
| 物体远离(距离=最大值) | values[0] = 5.0f | 屏幕点亮 |
| 物体靠近(距离=阈值边界) | values[0] = 1.0f | 取决于你的业务逻辑 |
| 传感器数据异常(负值) | values[0] = -1.0f | 忽略或报错 |
@Test
public void testOnSensorChanged_proximityFar() {
SensorEvent mockEvent = mock(SensorEvent.class);
mockEvent.values = new float[]{5.0f}; // 物体远离
// ... 捕获监听器并触发回调 ...
assertFalse(activity.isScreenDimmed());
}
@Test
public void testOnSensorChanged_negativeValue() {
SensorEvent mockEvent = mock(SensorEvent.class);
mockEvent.values = new float[]{-1.0f}; // 异常数据
// ... 触发回调 ...
// 验证没有调用任何屏幕操作
// 或者验证日志中输出了错误信息
}
我曾经遇到过一个bug:某款手机的距离传感器在物体靠近时返回的不是0,而是0.5。我们的代码只判断了== 0,结果屏幕死活不灭。从那以后,我写测试时一定会加上边界值测试。
集成测试:把Mock和真机结合起来
单元测试能保证逻辑正确,但传感器和系统的交互还得靠集成测试。Android提供了AndroidJUnitRunner,可以在模拟器或真机上跑测试。
不过,集成测试里我们依然可以用Mockito来模拟部分依赖。比如,你不想真的去操作传感器硬件,但想验证UI的响应。
@RunWith(AndroidJUnit4.class)
public class DistanceSensorIntegrationTest {
@Rule
public ActivityTestRule<MainActivity> activityRule =
new ActivityTestRule<>(MainActivity.class);
@Test
public void testSensorIntegration() {
MainActivity activity = activityRule.getActivity();
// 这里可以获取真实的SensorManager
SensorManager realManager = activity.getSensorManager();
// 但我们可以用spy来部分模拟
SensorManager spyManager = spy(realManager);
activity.setSensorManager(spyManager);
// 然后对特定方法进行mock
Sensor mockSensor = mock(Sensor.class);
when(spyManager.getDefaultSensor(Sensor.TYPE_PROXIMITY))
.thenReturn(mockSensor);
// 执行操作并验证
activity.registerSensor();
// ...
}
}
这里用了spy而不是mock。spy会保留真实对象的大部分行为,只替换你指定的方法。这样,你既能用到真实的SensorManager环境,又能控制特定方法的返回值。
测试异步回调的陷阱
传感器事件是异步的。你的测试代码调用registerListener后,onSensorChanged不会立即触发。如果你直接断言结果,大概率会失败。
解决方案有两种:
- 手动触发: 就像前面用ArgumentCaptor那样,自己调用回调方法。这是最可控的方式。
- 使用CountDownLatch: 如果回调是在后台线程触发的,可以用锁等待。
@Test
public void testAsyncSensorEvent() throws InterruptedException {
CountDownLatch latch = new CountDownLatch(1);
final boolean[] result = {false};
SensorEventListener listener = new SensorEventListener() {
@Override
public void onSensorChanged(SensorEvent event) {
result[0] = true;
latch.countDown();
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {}
};
// 模拟传感器事件在1秒后触发
new Thread(() -> {
try {
Thread.sleep(1000);
listener.onSensorChanged(mock(SensorEvent.class));
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
// 等待最多2秒
latch.await(2, TimeUnit.SECONDS);
assertTrue(result[0]);
}
不过说实话,在单元测试里我很少用CountDownLatch。因为Mockito已经能同步触发回调了,没必要引入异步的复杂性。
总结一下测试套路
写传感器测试,其实就三步:
- Mock依赖: 把SensorManager、Sensor、SensorEvent都mock掉
- 设定行为: 用when().thenReturn()控制返回值
- 触发并验证: 手动调用回调,用verify()或assert检查结果
下面这张图,是我自己总结的测试流程,你可以参考一下:
这套流程我用了好几年,从早期的Android 4.4到现在的Android 14,基本没变过。你只要掌握了这个套路,不管什么传感器——距离、光线、加速度——测试方法都大同小异。
最后说一句:测试不是负担,是你的安全网。尤其是传感器这种跟硬件打交道的代码,出了bug很难排查。有了测试,你改代码时心里就有底了。