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 带到策略测试里来。
模拟场景测试
单元测试能验证单点逻辑,但真实场景往往是多个事件同时发生。比如:正在打电话,突然插入耳机,同时来了一条通知——这时候音频怎么路由?
模拟场景测试就是干这个的。我常用的方法有两种:
- 事件序列模拟:按时间顺序注入设备插拔、应用启动、焦点变化等事件
- 并发场景模拟:同时触发多个异步事件,检查策略是否出现竞态条件
我曾经在项目中遇到一个经典问题:蓝牙耳机连接时,如果同时有来电和媒体播放请求,策略会随机选择路由设备。后来我写了一个并发测试脚本,反复触发这两个事件,终于复现了那个概率性 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"]
)
注意:自动化脚本跑完后,一定要恢复设备状态。我吃过亏——脚本跑完没关蓝牙,下一个测试用例全被干扰了。建议每个用例前后都做状态清理。
测试策略的层次结构
这三种测试方法不是孤立的,它们构成了一个金字塔结构:
你看这个金字塔:底层是单元测试,跑得快、覆盖细;中层是模拟场景,抓组合逻辑;顶层是自动化脚本,做回归保障。我建议团队按这个比例分配测试精力——70% 单元测试,20% 场景测试,10% 自动化回归。
避坑指南
最后分享几个我踩过的坑:
- 别依赖真实硬件:单元测试里用 mock 设备,别插着真耳机跑。否则 CI 环境跑不了。
- 时间同步问题:模拟场景测试里,事件注入后要等策略引擎处理完。我习惯加一个
waitForPolicyIdle()方法,轮询状态直到稳定。 - 日志要打全:测试失败时,没有日志等于没有线索。我在每个测试用例里都加
dumpsys audio的日志输出,方便事后分析。
一句话总结:音频策略测试不是可选项,是必选项。单元测试保底,场景测试抓组合,自动化脚本守回归——三层防线缺一不可。
公众号:蓝海资料掘金营,微信deep3321