29、WiFi测试与模拟:使用Android Emulator模拟WiFi、使用adb模拟WiFi状态、单元测试与UI测试

说实话,很多开发者在做WiFi功能时,最容易忽略的就是测试环节。大家总觉得「连上路由器能上网就行了」,结果一到真机适配就翻车。我早期做WiFi项目时也吃过这个亏——在模拟器上跑得好好的,到了某款国产手机上直接闪退。后来我才意识到,WiFi测试不是可有可无的步骤,而是保证稳定性的底线。

这一章,我就带你系统梳理一下WiFi测试的几种手段。从模拟器模拟,到adb命令控制,再到单元测试和UI测试,咱们一步步来。

WiFi测试与模拟知识体系 WiFi测试与模拟 Android Emulator模拟WiFi 设置SSID/信号强度/加密类型 adb命令模拟WiFi状态 连接/断开/信号强度变化 单元测试 ViewModel / Repository 逻辑验证 创建虚拟网络 配置网络属性 adb shell cmd wifi 模拟网络变化 JUnit + Mockito UI测试 (Espresso) 覆盖开发全流程的测试策略

29.1 使用Android Emulator模拟WiFi

Android Emulator其实内置了WiFi模拟能力。很多人不知道,它默认就有一个虚拟的WiFi网络。你打开模拟器的设置,进入WiFi页面,就能看到已经连上了一个叫「AndroidWifi」的网络。

但光有默认网络还不够。有时候我们需要测试不同的WiFi环境,比如弱信号、不同加密方式、甚至多个可用网络。这时候就要手动配置了。

核心操作步骤:

  1. 打开模拟器的「Extended Controls」(扩展控制面板)
  2. 选择「Telephony」或「Cellular」旁边的「WiFi」选项卡
  3. 在这里可以添加虚拟WiFi网络,设置SSID、BSSID、加密类型(WPA2/WPA3/开放)
  4. 还可以调整信号强度,从-30 dBm(极强)到-90 dBm(极弱)

我个人习惯在测试WiFi扫描逻辑时,至少添加3-4个虚拟网络。一个信号强的,一个信号弱的,一个加密的,一个开放的。这样能覆盖大部分真实场景。

小技巧:模拟器的WiFi模拟只支持API 25以上的系统镜像。如果你用的是旧版镜像,可能找不到这个功能。建议用API 28以上的镜像。

29.2 使用adb模拟WiFi状态

模拟器图形界面虽然方便,但自动化测试时就不太够用了。这时候adb命令才是王道。

adb控制WiFi的核心命令是 adb shell cmd wifi。这个命令在Android 10及以上版本中功能非常强大。

# 查看当前WiFi状态
adb shell dumpsys wifi | grep "Wi-Fi"

# 启用/禁用WiFi
adb shell svc wifi enable
adb shell svc wifi disable

# 模拟连接到一个网络(需要先知道网络ID)
adb shell cmd wifi connect-network "MyTestWiFi" wpa2 "password123"

# 模拟断开当前连接
adb shell cmd wifi disconnect

# 模拟信号强度变化(需要root或使用模拟器)
adb shell cmd wifi set-wifi-enabled enabled

我曾经在做一个WiFi切换功能时,需要反复测试从弱信号网络切换到强信号网络的逻辑。手动操作太慢了,我就写了个adb脚本,每隔5秒切换一次信号强度,配合自动化测试跑了一整晚。第二天一看日志,抓到了三个边界情况下的崩溃。

注意:adb的 cmd wifi 命令在非root的真机上权限受限。模拟器上基本都能用,真机测试建议用 svc wifi 系列命令。另外,dumpsys wifi 的输出内容非常多,建议用grep过滤关键信息。

29.3 单元测试:验证WiFi逻辑的正确性

单元测试这块,说白了就是保证你的WiFi相关代码在脱离真机的情况下也能跑通。我见过太多人把网络请求、WiFi扫描这些逻辑直接写在Activity里,结果想测都没法测。

正确的做法是把WiFi操作封装到Repository或ViewModel中,然后用Mockito模拟WiFiManager的行为。

// 一个典型的WiFi Repository单元测试
@RunWith(MockitoJUnitRunner.class)
public class WiFiRepositoryTest {

    @Mock
    private WifiManager mockWifiManager;

    @Mock
    private ConnectivityManager mockConnectivityManager;

    private WiFiRepository repository;

    @Before
    public void setUp() {
        repository = new WiFiRepository(mockWifiManager, mockConnectivityManager);
    }

    @Test
    public void testIsWiFiEnabled_returnsTrue() {
        // 模拟WiFi已开启
        when(mockWifiManager.isWifiEnabled()).thenReturn(true);

        boolean result = repository.isWiFiEnabled();

        assertTrue(result);
        verify(mockWifiManager).isWifiEnabled();
    }

    @Test
    public void testScanResults_returnsList() {
        List<ScanResult> mockResults = new ArrayList<>();
        ScanResult result = new ScanResult();
        result.SSID = "TestNetwork";
        result.level = -50;
        mockResults.add(result);

        when(mockWifiManager.getScanResults()).thenReturn(mockResults);

        List<ScanResult> results = repository.getScanResults();

        assertEquals(1, results.size());
        assertEquals("TestNetwork", results.get(0).SSID);
    }
}

为什么我要强调单元测试?因为WiFi相关的系统服务在模拟器上行为可能和真机不一致。比如模拟器上的 getScanResults() 返回的结果列表,和真机上的排序规则可能不同。单元测试能帮你提前发现这些差异。

我的经验:写WiFi单元测试时,重点关注三个点:

  • WiFi开关状态的正确传递
  • 扫描结果列表的排序和过滤逻辑
  • 网络连接状态变化的回调处理

这三个点最容易出bug,也最适合用单元测试覆盖。

29.4 UI测试:用Espresso验证WiFi界面

单元测试保证了逻辑正确,但用户界面好不好用还得靠UI测试。Android官方推荐的UI测试框架是Espresso,配合AndroidX Test使用。

UI测试WiFi功能时,我一般会测试这几个场景:

  • WiFi开关打开后,列表是否显示扫描结果
  • 点击一个网络后,是否弹出密码输入框
  • 输入错误密码时,是否显示错误提示
  • WiFi关闭时,界面是否显示「未连接」状态
@RunWith(AndroidJUnit4.class)
public class WiFiListFragmentTest {

    @Rule
    public ActivityTestRule<MainActivity> activityRule =
            new ActivityTestRule<>(MainActivity.class);

    @Test
    public void testToggleWiFi_switchChangesState() {
        // 找到WiFi开关
        onView(withId(R.id.wifi_switch))
                .perform(click());

        // 验证开关状态已改变
        onView(withId(R.id.wifi_switch))
                .check(matches(isChecked()));

        // 验证列表开始显示扫描结果
        onView(withId(R.id.scan_results_list))
                .check(matches(isDisplayed()));
    }

    @Test
    public void testClickNetwork_showsPasswordDialog() {
        // 先确保WiFi已开启
        onView(withId(R.id.wifi_switch))
                .perform(click());

        // 点击列表中的第一个网络
        onView(withId(R.id.scan_results_list))
                .perform(RecyclerViewActions
                        .actionOnItemAtPosition(0, click()));

        // 验证密码对话框出现
        onView(withText("输入密码"))
                .check(matches(isDisplayed()));
    }
}

写UI测试时有个坑——模拟器上的WiFi扫描结果可能为空。我建议在测试前先用adb命令添加一个虚拟网络,或者用MockWebServer模拟网络数据。不然测试跑起来,列表是空的,点击操作就会失败。

避坑指南:我曾经在CI上跑UI测试,发现WiFi列表总是空的。查了半天,原来是模拟器启动后WiFi模块还没完全初始化。解决方案是在测试的 @Before 方法里加一个 SystemClock.sleep(2000),等WiFi服务稳定后再操作。

29.5 测试策略总结

说了这么多,其实就一个核心思想:不同阶段用不同的测试手段

测试类型 适用场景 优点 缺点
模拟器WiFi模拟 功能验证、UI调试 可视化、操作直观 无法自动化
adb命令模拟 自动化测试、压力测试 可脚本化、快速 需要熟悉命令
单元测试 逻辑验证、边界条件 快速、可靠、可重复 无法测试UI
UI测试 界面交互、用户体验 覆盖真实操作流程 运行慢、环境依赖

我个人习惯的开发流程是这样的:写代码时先用模拟器手动验证一遍,然后写单元测试覆盖核心逻辑,最后用adb脚本配合UI测试做回归。这样一套下来,WiFi功能基本不会出大问题。

嗯,测试这块就聊到这儿。记住一点:WiFi测试不是「有就行」,而是要「覆盖全」。你永远不知道用户会在什么样的网络环境下使用你的App——可能是信号满格的办公室,也可能是只有一格信号的电梯里。测试做得越细,线上翻车的概率就越低。


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