26、单元测试与模拟:使用Mockito模拟SensorEvent,编写传感器逻辑的单元测试。

说实话,很多做Android开发的朋友,一提到传感器测试就头大。

你想想看,总不能每次跑测试都拿着手机在桌上晃来晃去吧?真机测试当然要做,但单元测试这块,咱们得换个思路。我个人的习惯是——用Mockito把SensorEvent模拟出来,让测试代码自己“演”一遍传感器数据的变化。

为什么需要模拟SensorEvent?

传感器逻辑有个特点:它依赖硬件事件。没有真实的光线传感器,你的代码就跑不起来。但单元测试的核心诉求是——快速、可靠、可重复。

说白了,我们要的是“假数据,真逻辑”。

核心思路: 用Mockito创建一个假的SensorEvent对象,手动塞入values数组,然后验证你的回调逻辑是否按预期执行。

我在项目中遇到过好几次这样的坑:真机测试一切正常,但单元测试一跑就挂。后来发现,是因为SensorEvent的timestamp和accuracy没处理好。嗯,这里要注意,模拟的时候别漏了这些细节。

环境准备:引入Mockito依赖

首先,你得在build.gradle里加上Mockito。我建议用最新稳定版,别追新,稳定第一。

dependencies {
    testImplementation 'org.mockito:mockito-core:4.11.0'
    testImplementation 'org.mockito:mockito-inline:4.11.0'
}

mockito-inline这个库挺重要,它能帮你mock final类和static方法。SensorEvent本身不是final,但有些系统方法可能需要它。

第一步:模拟SensorEvent对象

Mockito最常用的方式就是 mock()when()。我们来模拟一个光线强度为 500 lux 的事件。

import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import android.hardware.Sensor;
import android.hardware.SensorEvent;

import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class LightSensorTest {

    @Mock
    private SensorEvent mockEvent;

    @Mock
    private Sensor mockSensor;

    @Test
    public void testLightSensorEvent() {
        // 模拟传感器类型为光线传感器
        when(mockSensor.getType()).thenReturn(Sensor.TYPE_LIGHT);

        // 模拟事件数据:values[0] = 500.0f
        float[] mockValues = {500.0f};
        when(mockEvent.sensor).thenReturn(mockSensor);
        when(mockEvent.values).thenReturn(mockValues);
        when(mockEvent.timestamp).thenReturn(System.currentTimeMillis());
        when(mockEvent.accuracy).thenReturn(SensorManager.SENSOR_STATUS_ACCURACY_HIGH);

        // 这里就可以把mockEvent传入你的业务逻辑了
        // 比如:lightSensorManager.onSensorChanged(mockEvent);
    }
}

你看,代码其实很直白。我刚开始用Mockito时,总想着要模拟整个SensorManager,后来发现没必要——你只需要模拟那个事件本身。

第二步:编写可测试的传感器逻辑

为了让测试更好写,我建议把传感器逻辑封装成一个独立的类。别把代码直接写在Activity或Fragment里,那样测试起来太痛苦了。

public class LightSensorManager {
    private float currentLux;
    private OnLightChangeListener listener;

    public interface OnLightChangeListener {
        void onLightChanged(float lux);
    }

    public void onSensorChanged(SensorEvent event) {
        if (event.sensor.getType() == Sensor.TYPE_LIGHT) {
            currentLux = event.values[0];
            if (listener != null) {
                listener.onLightChanged(currentLux);
            }
        }
    }

    public void setOnLightChangeListener(OnLightChangeListener listener) {
        this.listener = listener;
    }

    public float getCurrentLux() {
        return currentLux;
    }
}

这样设计的好处是——你的业务逻辑和Android框架解耦了。测试时只需要传入模拟的SensorEvent,就能验证所有逻辑。

第三步:编写完整的单元测试

我们来写一个完整的测试用例,覆盖正常情况和边界情况。

@Test
public void testLightSensorCallback() {
    // 创建被测试对象
    LightSensorManager manager = new LightSensorManager();

    // 模拟回调
    LightSensorManager.OnLightChangeListener mockListener =
            mock(LightSensorManager.OnLightChangeListener.class);
    manager.setOnLightChangeListener(mockListener);

    // 模拟事件:光线强度 1000 lux
    float[] values = {1000.0f};
    when(mockEvent.sensor.getType()).thenReturn(Sensor.TYPE_LIGHT);
    when(mockEvent.values).thenReturn(values);

    // 执行
    manager.onSensorChanged(mockEvent);

    // 验证回调被调用,且参数正确
    verify(mockListener).onLightChanged(1000.0f);
    assertEquals(1000.0f, manager.getCurrentLux(), 0.01f);
}

@Test
public void testLightSensorZeroLux() {
    LightSensorManager manager = new LightSensorManager();
    LightSensorManager.OnLightChangeListener mockListener =
            mock(LightSensorManager.OnLightChangeListener.class);
    manager.setOnLightChangeListener(mockListener);

    // 模拟黑暗环境:0 lux
    float[] values = {0.0f};
    when(mockEvent.sensor.getType()).thenReturn(Sensor.TYPE_LIGHT);
    when(mockEvent.values).thenReturn(values);

    manager.onSensorChanged(mockEvent);

    verify(mockListener).onLightChanged(0.0f);
    assertEquals(0.0f, manager.getCurrentLux(), 0.01f);
}

小技巧:ArgumentCaptor 可以捕获回调参数,做更细致的断言。比如你想验证回调被调用了两次,每次的值不同。

第四步:模拟多个传感器事件序列

有时候你需要模拟一连串的光线变化,比如从亮到暗再到亮。这时候可以用 when().thenReturn().thenReturn() 来模拟连续调用。

@Test
public void testMultipleLightChanges() {
    LightSensorManager manager = new LightSensorManager();
    LightSensorManager.OnLightChangeListener mockListener =
            mock(LightSensorManager.OnLightChangeListener.class);
    manager.setOnLightChangeListener(mockListener);

    // 模拟三次不同的光线值
    when(mockEvent.sensor.getType()).thenReturn(Sensor.TYPE_LIGHT);
    when(mockEvent.values)
            .thenReturn(new float[]{100.0f})   // 第一次
            .thenReturn(new float[]{500.0f})   // 第二次
            .thenReturn(new float[]{50.0f});   // 第三次

    // 依次触发
    manager.onSensorChanged(mockEvent);
    manager.onSensorChanged(mockEvent);
    manager.onSensorChanged(mockEvent);

    // 验证回调被调用了三次
    verify(mockListener, times(3)).onLightChanged(anyFloat());
    // 验证最终值是 50.0
    assertEquals(50.0f, manager.getCurrentLux(), 0.01f);
}

我曾经在做一个自动亮度调节功能时,就靠这种序列模拟发现了逻辑漏洞——当光线从亮突然变暗时,我的代码没有做平滑过渡,导致屏幕亮度闪了一下。单元测试帮我提前抓到了这个bug。

常见坑点与避坑指南

注意: SensorEvent的values数组长度不固定。光线传感器只有values[0],但其他传感器可能有多个值。模拟时一定要确认数组长度。

  • 坑点1: 忘记设置timestamp。有些逻辑会依赖时间戳做滤波,不设的话可能触发空指针。
  • 坑点2: accuracy没设对。SENSOR_STATUS_UNRELIABLE 和 SENSOR_STATUS_ACCURACY_HIGH 的处理逻辑完全不同。
  • 坑点3: 直接new SensorEvent()。SensorEvent的构造函数是隐藏的,必须用mock。

我曾经有一次,因为没设accuracy,导致测试一直通过但真机上闪退。排查了半天才发现,原来我的滤波算法在accuracy为0时直接抛异常了。

知识体系总览

下面这张图帮你理清本章的核心脉络:

Mockito模拟SensorEvent单元测试知识体系 SensorEvent模拟 环境准备 模拟SensorEvent 可测试业务封装 验证与断言 引入Mockito依赖 配置testImplementation mock()创建对象 when().thenReturn()设值 封装LightSensorManager 解耦Android框架 verify()检查回调 assertEquals()断言值

总结

用Mockito模拟SensorEvent,说白了就是三步走:mock对象、设值、验证。别把它想得太复杂。

我个人建议,每个传感器相关的业务类,都配上对应的单元测试。刚开始写可能会觉得麻烦,但当你改了一次代码,跑一遍测试就发现所有问题的时候,你会感谢当初的自己。

嗯,今天就聊到这儿。记住,测试不是为了应付KPI,而是为了让你睡得安稳。

核心要点回顾:

  • 用Mockito的mock()创建SensorEvent,不要直接new
  • 记得设置sensor类型、values数组、timestamp和accuracy
  • 把传感器逻辑封装成独立类,方便测试
  • 用verify()验证回调,用assertEquals()断言状态
  • 多写边界测试:0 lux、最大值、连续变化

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