23、蓝牙自动化测试:Android Instrumentation Test、BLE模拟器与自动化脚本

说实话,蓝牙开发最让人头疼的不是写代码,而是测试。手动测试?一次两次还行,反复几十次你肯定会崩溃。我当年做一个智能门锁项目,每天要重复配对、通信、断连几十遍,手指都快按出腱鞘炎了。后来我下定决心,必须搞自动化。

这一章,我就把压箱底的自动化测试方案拿出来。咱们分三块讲:Android 自带的 Instrumentation Test、Nordic 的 BLE 模拟器、还有用 Python+adb 写自动化脚本。这三板斧用好了,测试效率能翻好几倍。

23.1 Android Instrumentation Test:让 App 自己测自己

Instrumentation Test 是 Android 官方提供的测试框架。说白了,就是写一段代码,让 App 自动执行一系列操作,然后检查结果对不对。

为什么要用它测蓝牙?因为蓝牙操作涉及系统服务、权限、状态机,纯单元测试很难覆盖全。Instrumentation Test 跑在真机上,能真实调用蓝牙 API。

23.1.1 环境配置

在 app/build.gradle 里加上依赖:

androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0'

嗯,这里要注意,UiAutomator 在 Android 10 以上需要额外权限,记得在测试代码里加 @GrantPermission 注解。

23.1.2 写一个蓝牙扫描测试

我习惯把测试用例按功能分组。先来个扫描测试:

@RunWith(AndroidJUnit4.class)
public class BleScanTest {

    private BluetoothLeScanner scanner;
    private CountDownLatch latch;

    @Before
    public void setUp() {
        BluetoothManager manager = (BluetoothManager) 
            InstrumentationRegistry.getInstrumentation().getTargetContext()
                .getSystemService(Context.BLUETOOTH_SERVICE);
        BluetoothAdapter adapter = manager.getAdapter();
        scanner = adapter.getBluetoothLeScanner();
    }

    @Test
    public void testScanForDevices() throws InterruptedException {
        latch = new CountDownLatch(1);
        ScanCallback callback = new ScanCallback() {
            @Override
            public void onScanResult(int callbackType, ScanResult result) {
                // 只要扫描到设备就算通过
                assertNotNull(result.getDevice());
                latch.countDown();
            }
        };

        scanner.startScan(callback);
        boolean scanned = latch.await(10, TimeUnit.SECONDS);
        assertTrue("10秒内未扫描到设备", scanned);
        scanner.stopScan(callback);
    }
}

这段代码的核心是 CountDownLatch。为什么用它?因为蓝牙扫描是异步的,你不能直接 assert,得等回调回来。我刚开始写的时候没用这个,结果测试永远通过——因为 assert 在回调之前就执行完了,根本没检查到任何东西。

23.1.3 配对与通信测试

配对测试稍微复杂点。你需要先让设备进入可发现模式,然后调用 createBond

@Test
public void testPairing() throws Exception {
    BluetoothDevice device = ... // 从扫描结果获取
    Method method = device.getClass().getMethod("createBond");
    boolean result = (boolean) method.invoke(device);
    assertTrue("配对失败", result);

    // 等待配对完成
    Thread.sleep(3000);
    assertEquals(BluetoothDevice.BOND_BONDED, device.getBondState());
}

注意:createBond 是隐藏 API,在 Android 10+ 上可能被限制。我建议用 BroadcastReceiver 监听配对状态变化,而不是直接 sleep。

23.2 BLE 模拟器:Nordic nRF Connect SDK

真机测试有个痛点——你不可能每次都有完整的硬件设备。尤其是早期开发阶段,硬件可能还没出来。这时候就需要 BLE 模拟器了。

Nordic 的 nRF Connect SDK 提供了 PC 端的 BLE 模拟器。它可以在你的电脑上模拟一个完整的 BLE 外设,包括 Service、Characteristic、Notification。

23.2.1 搭建模拟外设

用 nRF Connect 桌面版,新建一个模拟设备:

  1. 打开 nRF Connect,点击「Create Configuration」
  2. 添加 Service:比如 0xFFE0(自定义服务)
  3. 添加 Characteristic:0xFFE1,属性设为 Read/Write/Notify
  4. 启动模拟器,设备就开始广播了

你想想看,这比拿真机调试方便多了。我在做心率监测 App 的时候,就是先用模拟器生成各种心率数据,把 UI 和逻辑调通,最后才连真机。

23.2.2 模拟异常场景

模拟器还能干一件事——模拟异常。比如:

  • 断开连接:模拟信号中断
  • 延迟响应:模拟网络延迟
  • 错误数据:模拟设备返回乱码

这些场景在真机上很难复现,但用模拟器一键就能搞定。我曾经遇到过一个 bug,设备偶尔会返回长度不对的数据包,用真机测了一周没复现,换模拟器半小时就找到了。

23.3 Python + adb:自动化脚本三板斧

Instrumentation Test 适合测 App 内部逻辑,但有些场景它搞不定。比如:

  • 系统蓝牙开关控制
  • 多设备配对流程
  • 长时间稳定性测试

这时候就该 Python + adb 上场了。说白了,就是用 Python 调用 adb 命令,模拟用户操作。

23.3.1 控制蓝牙开关

import subprocess

def toggle_bluetooth(on=True):
    state = "enable" if on else "disable"
    cmd = f"adb shell svc bluetooth {state}"
    subprocess.run(cmd, shell=True)

def is_bluetooth_on():
    result = subprocess.run(
        "adb shell dumpsys bluetooth_manager | grep 'state:'",
        shell=True, capture_output=True, text=True
    )
    return "STATE_ON" in result.stdout

这里用 svc bluetooth 而不是 settings 命令,因为它不需要 root 权限。我踩过这个坑,一开始用 settings 命令,结果在部分手机上没效果。

23.3.2 自动化配对脚本

配对流程通常涉及点击系统弹窗。用 adb 模拟点击:

def pair_device(address):
    # 1. 打开蓝牙设置
    subprocess.run("adb shell am start -a android.settings.BLUETOOTH_SETTINGS", shell=True)
    time.sleep(2)

    # 2. 点击扫描到的设备
    subprocess.run(f"adb shell input tap 500 800", shell=True)
    time.sleep(3)

    # 3. 确认配对弹窗
    subprocess.run("adb shell input tap 540 1200", shell=True)
    time.sleep(5)

    # 4. 检查配对状态
    result = subprocess.run(
        f"adb shell dumpsys bluetooth_manager | grep {address}",
        shell=True, capture_output=True, text=True
    )
    return "BOND_BONDED" in result.stdout

小技巧:不同手机的弹窗位置不一样。我一般先用 adb shell uiautomator dump 导出当前界面布局,然后解析 XML 找到按钮坐标。这样脚本就能适配不同分辨率。

23.3.3 通信压力测试

长时间通信测试,我写过一个循环脚本:

def stress_test(iterations=100):
    for i in range(iterations):
        # 发送数据
        subprocess.run("adb shell am broadcast -a com.example.SEND_DATA --es data 'hello'", shell=True)
        time.sleep(0.5)

        # 检查日志
        log = subprocess.run(
            "adb logcat -d -s BleService:I | tail -5",
            shell=True, capture_output=True, text=True
        )
        if "ERROR" in log.stdout:
            print(f"第{i+1}次通信异常")
            break

        print(f"第{i+1}次通信正常")
        time.sleep(1)

这个脚本我跑过一整夜,第二天早上看结果,能发现很多偶发问题。比如内存泄漏、连接超时、数据丢包,这些在手动测试时很难察觉。

23.4 知识体系总览

下面这张图,我把本章的核心逻辑串起来了:

蓝牙自动化测试知识体系 Instrumentation Test BLE 模拟器 Python + adb 测试内容 • 蓝牙扫描测试 • 配对与通信测试 • 状态机验证 模拟能力 • 模拟外设广播 • 模拟异常场景 • 数据注入 自动化操作 • 蓝牙开关控制 • 自动化配对 • 压力测试 高效、稳定、可复现的测试

23.5 实战建议

最后,我根据多年经验,给你几条实在的建议:

  • 先模拟后真机:用 nRF Connect 模拟器把逻辑调通,再上真机。能省一半时间。
  • 日志要打全:自动化测试最怕失败后不知道原因。每个关键步骤都打日志,包括时间戳、状态、数据内容。
  • 异常处理要到位:蓝牙操作经常超时,脚本里一定要加 try-except 和重试机制。我见过太多脚本跑一半就挂在那里的情况。
  • CI/CD 集成:把自动化测试集成到 Jenkins 或 GitHub Actions 里,每次提交代码自动跑一遍。这样能尽早发现问题。

核心要点:Instrumentation Test 测 App 逻辑,BLE 模拟器测设备交互,Python+adb 测系统流程。三者结合,才能覆盖蓝牙开发的全部测试场景。

好了,这一章的内容就到这里。自动化测试是个持续投入的过程,一开始写脚本可能觉得麻烦,但一旦跑起来,你会发现它帮你省下的时间远超预期。


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