24、单元测试与集成测试:使用Mockito模拟SensorManager、编写传感器相关测试用例

说实话,很多做Android开发的朋友,一听到「测试」两个字就头大。尤其是传感器这块,硬件不在手里,模拟器又不靠谱,怎么测?

我早年刚接手一个距离传感器项目时,也踩过这个坑。代码写完了,跑真机一测,发现逻辑全乱套。后来我学乖了——先把测试写好,再调硬件。今天咱们就聊聊,怎么用Mockito把SensorManager给「骗」过去,写出靠谱的传感器测试用例。

为什么需要Mock SensorManager?

你想想看,距离传感器是硬件设备。你的测试代码跑在开发机上,根本没有真实的传感器数据。如果直接new一个SensorManager,那测试一运行就报空指针,或者压根拿不到数据。

说白了,我们需要一个「替身」。这个替身能假装自己是SensorManager,你想让它返回什么数据,它就返回什么数据。Mockito就是干这个的。

核心思路: 用Mockito创建SensorManager的模拟对象,控制传感器事件的触发时机和数据内容,从而验证你的业务逻辑是否正确。

搭建测试环境

首先,在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方法。

小技巧: 如果你觉得ArgumentCaptor写起来麻烦,也可以把监听器暴露成包级可见的成员变量,测试里直接访问。不过我个人不太推荐,会破坏封装性。

测试多种传感器数据场景

距离传感器不是只有「靠近」和「远离」两种状态。有些设备会返回不同的数值范围,比如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环境,又能控制特定方法的返回值。

注意: 集成测试跑在真机或模拟器上,速度比单元测试慢很多。我建议把集成测试放在CI的夜间构建里跑,白天开发时主要跑单元测试。

测试异步回调的陷阱

传感器事件是异步的。你的测试代码调用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已经能同步触发回调了,没必要引入异步的复杂性。

总结一下测试套路

写传感器测试,其实就三步:

  1. Mock依赖: 把SensorManager、Sensor、SensorEvent都mock掉
  2. 设定行为: 用when().thenReturn()控制返回值
  3. 触发并验证: 手动调用回调,用verify()或assert检查结果

下面这张图,是我自己总结的测试流程,你可以参考一下:

传感器单元测试流程 1. 创建Mock对象 @Mock SensorManager 2. 设定返回值 when().thenReturn() 3. 执行被测方法 registerSensor() 4. 捕获监听器 ArgumentCaptor 5. 手动触发回调 listener.onSensorChanged() 6. 验证结果 verify() / assert 关键点: • 所有传感器依赖都要mock,包括SensorEvent对象 • 用ArgumentCaptor获取注册的监听器,手动触发回调

这套流程我用了好几年,从早期的Android 4.4到现在的Android 14,基本没变过。你只要掌握了这个套路,不管什么传感器——距离、光线、加速度——测试方法都大同小异。

最后说一句:测试不是负担,是你的安全网。尤其是传感器这种跟硬件打交道的代码,出了bug很难排查。有了测试,你改代码时心里就有底了。


公众号:蓝海资料掘金营,微信deep3321