第二十六章:测试与 Mock:UnitTest 框架、MockWifiService、集成测试

说实话,写 Wi-Fi 服务最怕什么?不是逻辑复杂,而是改了一行代码,不知道把哪里搞崩了。Wi-Fi 这东西涉及硬件、驱动、框架、应用层,牵一发动全身。我早年吃过这个亏——改了个扫描逻辑,结果某些机型上连不上隐藏网络,查了两天才发现是单元测试没覆盖到。

所以这一章,咱们聊聊怎么给 Wi-Fi Service 做测试。说白了,就是给你的代码上保险。

为什么 Wi-Fi Service 测试这么难搞?

你想想看,Wi-Fi Service 依赖的东西太多了:

  • 硬件状态(Wi-Fi 芯片开没开)
  • 系统服务(WifiManager、ConnectivityManager)
  • 异步回调(扫描结果、连接状态)
  • 权限校验

这些在单元测试里都不好模拟。我见过不少项目,Wi-Fi 模块的测试覆盖率不到 10%。不是不想写,是真的不好写。

那怎么办?答案是:Mock。

UnitTest 框架选型

Android 这边,主流的单元测试框架就那几个:

框架 适用场景 我的评价
JUnit 4/5 基础单元测试 必选,没啥好说的
Mockito Mock 依赖对象 最常用,我基本每个项目都用
PowerMock Mock 静态方法、final 类 能不用就不用,有坑
Robolectric 模拟 Android 框架 集成测试利器

我个人习惯是:纯逻辑用 JUnit + Mockito,涉及 Android 框架用 Robolectric

MockWifiService:把硬件“骗”过去

Wi-Fi Service 里最难测的是什么?是那些跟硬件交互的方法。比如 startScan(),你没法在测试里真的去扫 Wi-Fi 吧?

所以我们要造一个假的 Wi-Fi 服务——MockWifiService。

来看个例子。假设我们有这样一个真实的服务:

public class WifiServiceImpl {
    private final WifiNative mWifiNative;

    public boolean startScan() {
        if (!mWifiNative.isDriverLoaded()) {
            return false;
        }
        return mWifiNative.scan();
    }
}

测试的时候,我们不想真的加载驱动。那就 Mock 掉 WifiNative

@Test
public void testStartScan_driverNotLoaded_returnsFalse() {
    // 准备
    WifiNative mockNative = mock(WifiNative.class);
    when(mockNative.isDriverLoaded()).thenReturn(false);

    WifiServiceImpl service = new WifiServiceImpl(mockNative);

    // 执行
    boolean result = service.startScan();

    // 验证
    assertFalse(result);
    verify(mockNative, never()).scan();
}

这里的关键是:把依赖“注入”进去,而不是在内部 new 出来。这样测试时就能替换成 Mock 对象。

我的小技巧:写 Wi-Fi Service 的时候,所有跟硬件、系统服务打交道的类,都定义成接口。这样测试时直接 Mock 接口,省事多了。

集成测试:把整个流程跑通

单元测试只能测单个方法。但 Wi-Fi 的流程往往是跨模块的——比如“用户点击连接 → 框架层发起连接 → 驱动层配置网络 → 返回结果”。

这种场景,就得靠集成测试了。

我一般用 Robolectric 来做。它能模拟 Android 的运行环境,让你在 JVM 上跑测试,不用真机。

@RunWith(RobolectricTestRunner.class)
public class WifiServiceIntegrationTest {

    @Test
    public void testConnectFlow() {
        // 模拟系统服务
        ShadowApplication shadowApp = Shadows.shadowOf(ApplicationProvider.getApplicationContext());
        shadowApp.setSystemService(Context.WIFI_SERVICE, new MockWifiManager());

        // 执行连接
        WifiManager wifiManager = (WifiManager) shadowApp.getApplicationContext()
                .getSystemService(Context.WIFI_SERVICE);
        boolean connected = wifiManager.connect(new WifiConfiguration(), null);

        // 验证状态
        assertTrue(connected);
        // 检查是否调用了底层的 connectNative()
    }
}

嗯,这里要注意:Robolectric 的 Shadow 对象不是万能的。有些底层操作它模拟不了,比如真正的驱动加载。这时候就得靠 Mock 来补位。

避坑指南

我曾经踩过的坑:
  • Mock 了不该 Mock 的东西——比如把 WifiManager 整个 Mock 掉,结果测试通过了,但上线后发现 WifiManager 的某些行为跟 Mock 的不一样。教训是:只 Mock 你控制不了的依赖,不要 Mock 你要测的类本身。
  • 异步回调没处理好——Wi-Fi 的很多操作是异步的(扫描、连接)。测试里要用 ArgumentCaptor 捕获回调,或者用 CountDownLatch 等待异步完成。
  • 忘了重置 Mock 状态——多个测试用例共用 Mock 对象时,记得在每个测试前重置,否则上一个测试的调用记录会影响下一个。

测试金字塔在 Wi-Fi 模块的应用

我一般这样分配测试比例:

  • 单元测试(70%):测每个 Manager、Service 的独立逻辑
  • 集成测试(20%):测跨模块的流程,比如扫描→结果回调→UI 更新
  • 端到端测试(10%):在真机上跑,测完整的用户操作路径

这个比例不是拍脑袋定的。我试过把单元测试压到 50%,结果集成测试跑起来一堆问题,定位还特别慢。后来老老实实把单元测试补上去,反而整体效率更高了。

SVG:Wi-Fi Service 测试架构图

下面这张图,是我做 Wi-Fi 测试时的分层思路:

Wi-Fi Service 测试分层架构 单元测试层(Unit Test) JUnit + Mockito 测试单个类/方法 · Mock 掉硬件依赖 · 覆盖边界条件 集成测试层(Integration Test) Robolectric + MockWifiService 模拟 Android 框架 · 测试跨模块流程 · 验证回调与状态 端到端测试层(E2E Test) 真机 + UI Automator / Espresso 模拟用户操作 · 验证完整链路 · 覆盖真实硬件场景 测试成本递增 →

从下往上看,测试成本越来越高,但覆盖的场景也越来越真实。我建议你从单元测试做起,先把逻辑层测稳了,再往上走。

总结一下

Wi-Fi Service 的测试,说白了就是三件事:

  1. 用 Mock 把硬件和系统服务“架空”,让测试只关注你的业务逻辑
  2. 用 Robolectric 模拟 Android 环境,跑通跨模块的流程
  3. 在真机上做最后的验证,确保跟硬件交互没问题

我个人的经验是:测试代码写得好不好,直接决定了你改代码的时候敢不敢重构。如果你每次改 Wi-Fi 代码都提心吊胆,那多半是测试没到位。

嗯,这一章就到这儿。代码写完了,别忘了跑一遍测试再提交。


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