22、音频策略测试方法:单元测试框架(AudioPolicyTest)、模拟场景测试、自动化测试脚本

音频策略写完了,怎么证明它是对的?

说实话,这个问题我踩过不少坑。早年做项目时,策略代码改了一行,结果蓝牙通话的路由全乱了。测试不到位,上线就翻车。所以今天咱们聊聊音频策略的测试方法——怎么用单元测试、模拟场景和自动化脚本,把策略逻辑焊死在正确轨道上。

单元测试框架:AudioPolicyTest

先说说单元测试。Android 原生提供了一个叫 AudioPolicyTest 的测试框架,专门用来验证策略引擎的决策逻辑。

我个人习惯把测试分成三层:

  • 策略规则测试:检查某个输入条件下,策略是否返回预期的输出设备
  • 优先级测试:多个设备同时可用时,策略是否按优先级排序
  • 边界条件测试:设备断开、超时、异常状态下的行为

举个例子,测试“插入有线耳机时,媒体音频路由到耳机”:

// AudioPolicyTest 示例
@Test
public void testWiredHeadsetMediaRouting() {
    // 模拟耳机插入
    addDevice(AudioDeviceInfo.TYPE_WIRED_HEADSET);
    
    // 模拟媒体播放请求
    AudioPolicy policy = new AudioPolicy(...);
    AudioDeviceInfo result = policy.getDeviceForStrategy(
        AudioSystem.STRATEGY_MEDIA);
    
    // 验证路由到耳机
    assertEquals(AudioDeviceInfo.TYPE_WIRED_HEADSET, 
                 result.getType());
}

这里有个坑——测试环境必须干净。我遇到过测试用例互相干扰的情况,因为前一个用例没清理设备状态。所以每个测试方法开头,我都会调用 resetAudioPolicy() 重置策略引擎。

核心要点:单元测试只测策略逻辑,不测硬件驱动。别把 HAL 层的 bug 带到策略测试里来。

模拟场景测试

单元测试能验证单点逻辑,但真实场景往往是多个事件同时发生。比如:正在打电话,突然插入耳机,同时来了一条通知——这时候音频怎么路由?

模拟场景测试就是干这个的。我常用的方法有两种:

  1. 事件序列模拟:按时间顺序注入设备插拔、应用启动、焦点变化等事件
  2. 并发场景模拟:同时触发多个异步事件,检查策略是否出现竞态条件

我曾经在项目中遇到一个经典问题:蓝牙耳机连接时,如果同时有来电和媒体播放请求,策略会随机选择路由设备。后来我写了一个并发测试脚本,反复触发这两个事件,终于复现了那个概率性 bug。

模拟场景测试的代码结构大概是这样:

// 模拟场景测试框架
public class AudioScenarioTest {
    
    @Test
    public void testCallWithHeadsetInsertion() {
        // 步骤1:建立通话
        simulateCallStart();
        
        // 步骤2:插入有线耳机
        simulateHeadsetPlug(true);
        
        // 步骤3:等待策略更新
        waitForPolicyUpdate(100);
        
        // 步骤4:验证路由
        assertRouteToDevice(AudioSystem.STREAM_VOICE_CALL,
                            AudioDeviceInfo.TYPE_WIRED_HEADSET);
    }
}

小技巧:模拟场景测试最好用真实设备或模拟器,别用 mock。因为有些策略决策依赖底层音频 HAL 的状态,mock 容易漏掉边界情况。

自动化测试脚本

手动测试跑一遍还行,但每次改代码都跑一遍?不现实。所以自动化脚本是必须的。

我通常用 Python 写自动化脚本,通过 adb 命令控制设备,配合 dumpsys audio 抓取策略状态。脚本的核心流程是:

  • 连接设备 → 重置音频状态 → 注入测试事件 → 抓取策略输出 → 比对预期结果

下面是一个简化版的自动化脚本框架:

# audio_policy_test_runner.py
import subprocess
import time

def run_scenario(scenario_name, commands, expected_routes):
    """执行一个测试场景"""
    # 重置设备音频状态
    adb("shell dumpsys audio reset")
    
    # 执行测试命令序列
    for cmd in commands:
        adb(cmd)
        time.sleep(0.5)
    
    # 抓取当前路由状态
    output = adb("shell dumpsys audio | grep 'routing'")
    
    # 比对预期结果
    for route in expected_routes:
        if route not in output:
            print(f"[FAIL] {scenario_name}: 期望 {route} 未命中")
            return False
    print(f"[PASS] {scenario_name}")
    return True

# 测试用例:蓝牙连接后媒体路由
run_scenario(
    "蓝牙媒体路由",
    commands=[
        "shell svc bluetooth enable",
        "shell am broadcast -a android.intent.action.HEADSET_PLUG --ei state 1"
    ],
    expected_routes=["device=BLUETOOTH_A2DP"]
)

注意:自动化脚本跑完后,一定要恢复设备状态。我吃过亏——脚本跑完没关蓝牙,下一个测试用例全被干扰了。建议每个用例前后都做状态清理。

测试策略的层次结构

这三种测试方法不是孤立的,它们构成了一个金字塔结构:

单元测试 (AudioPolicyTest) 覆盖策略规则、优先级、边界条件 模拟场景测试 事件序列、并发场景、竞态条件 自动化测试脚本 回归测试、持续集成 测试覆盖度递增 执行速度递减

你看这个金字塔:底层是单元测试,跑得快、覆盖细;中层是模拟场景,抓组合逻辑;顶层是自动化脚本,做回归保障。我建议团队按这个比例分配测试精力——70% 单元测试,20% 场景测试,10% 自动化回归。

避坑指南

最后分享几个我踩过的坑:

  • 别依赖真实硬件:单元测试里用 mock 设备,别插着真耳机跑。否则 CI 环境跑不了。
  • 时间同步问题:模拟场景测试里,事件注入后要等策略引擎处理完。我习惯加一个 waitForPolicyIdle() 方法,轮询状态直到稳定。
  • 日志要打全:测试失败时,没有日志等于没有线索。我在每个测试用例里都加 dumpsys audio 的日志输出,方便事后分析。

一句话总结:音频策略测试不是可选项,是必选项。单元测试保底,场景测试抓组合,自动化脚本守回归——三层防线缺一不可。


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