Appium结合ADB实现Android语音通话自动化测试实战
2026/7/5 9:36:19 网站建设 项目流程

1. 项目概述:当自动化测试遇上语音通话

最近在做一个移动端IM项目的测试,里面集成了语音通话功能。每次版本迭代,测试同学都要抱着十几台手机,手动拨号、接听、挂断,再检查通话质量,一套流程下来,半天就没了。更头疼的是,网络抖动、弱网环境这些场景,手动模拟起来既不准又麻烦。这让我开始琢磨,能不能把这套流程自动化起来?毕竟,重复、枯燥、易出错的活儿,正是自动化测试的用武之地。

在移动端自动化测试领域,Appium是绕不开的名字。它支持Android和iOS双平台,用一套脚本就能搞定,这对于我们这种需要覆盖多端、多机型的团队来说,吸引力巨大。但Appium的常规操作,比如点击按钮、输入文本、滑动列表,大家都很熟了。真正让我觉得有挑战的,是“语音通话”这个场景。它不仅仅是UI交互,还涉及到音频流的建立、传输和中断,以及背后的系统级调用。这要求我们的自动化脚本,不仅要能“看得见”(操作UI),还得能“听得见”(控制音频)和“说得了”(模拟终端指令)。

所以,今天我想分享的,就是如何利用Appium,并结合一些终端(ADB)控制技巧,来实现一个相对完整的语音通话自动化测试方案。这个方案不仅能模拟拨号、接听、挂断等基础流程,还能深入到通话质量验证的层面,比如通过系统接口获取通话状态、模拟网络切换等。如果你也在为语音通话这类涉及底层硬件的功能测试而头疼,希望这篇实战总结能给你一些直接的参考。

2. 环境搭建与核心工具链解析

工欲善其事,必先利其器。要实现语音通话的自动化,光有Appium还不够,我们需要一套能打通从UI到系统底层的工具链。

2.1 Appium服务端与客户端配置

首先,Appium本身是一个C/S架构。我们需要启动一个Appium服务器,然后通过客户端(用Python、Java等语言编写)发送指令。

对于Python环境,我推荐使用appium-python-client库。安装很简单:

pip install Appium-Python-Client

但这里有个关键点:Appium服务器版本与客户端库版本的兼容性。我踩过坑,新版的服务器搭配旧版的客户端库,经常会出现一些莫名其妙的协议错误。我的经验是,尽量保持两者版本同步更新。你可以通过以下命令分别查看和安装指定版本:

# 安装Appium服务器(全局安装) npm install -g appium@latest # 或者使用npx避免全局污染 npx appium@latest # 安装指定版本的Python客户端 pip install Appium-Python-Client==2.11.1

启动Appium服务器时,我习惯加上一些参数来获取更详细的日志,便于调试:

appium --log-level debug --allow-insecure chromedriver_autodownload

--log-level debug会在控制台输出非常详细的通信日志,当脚本执行出错时,这是定位问题根源的第一手资料。--allow-insecure参数则是为了解决一些新版本的安全限制,比如自动下载ChromeDriver。

2.2 Android测试环境深度配置

我们的主战场是Android,因此ADB(Android Debug Bridge)是另一个核心工具。它是我们与设备系统层对话的桥梁。

1. 设备连接与授权:确保设备通过USB连接后,执行adb devices能看到设备并显示为device状态(而不是unauthorized)。如果是无线调试,需要先用USB连接执行adb tcpip 5555,然后adb connect 设备IP:5555

2. 关键ADB命令准备:语音通话测试会频繁用到以下几个ADB命令,建议提前熟悉:

  • adb shell dumpsys telecom:获取当前系统的通话状态详情,这是验证通话是否成功建立的核心命令。
  • adb shell input keyevent KEYCODE_CALL:模拟按下“拨号”硬件键。
  • adb shell input keyevent KEYCODE_ENDCALL:模拟按下“挂断”硬件键。
  • adb shell screenrecord:录制屏幕,用于后期回溯测试过程,特别是验证UI状态。
  • adb shell monkey:可以用于压力测试期间,随机触发其他事件,测试通话的稳定性。

3. 权限授予:被测应用(通常是系统拨号盘或你自己的IM应用)需要获取电话、麦克风、联系人等敏感权限。在自动化开始前,可以通过ADB一键授予:

adb shell pm grant <package_name> android.permission.CALL_PHONE adb shell pm grant <package_name> android.permission.RECORD_AUDIO # ... 其他所需权限

这样可以避免脚本执行时被系统的权限弹窗阻塞。

2.3 构建Desired Capabilities:连接设备的“身份证”

Desired Capabilities是一组发送给Appium服务器的键值对,用于告诉服务器你想如何启动会话。对于语音通话测试,配置需要格外细致。

from appium import webdriver from appium.options.android import UiAutomator2Options # 使用新的Options模式(推荐) options = UiAutomator2Options() options.platform_name = 'Android' options.device_name = '你的设备名称' # 在`adb devices`中看到的名字 options.automation_name = 'UiAutomator2' # Android的自动化引擎 options.app_package = 'com.android.dialer' # 系统拨号盘包名 options.app_activity = '.main.impl.MainActivity' # 启动Activity # 关键配置:不重置应用状态,避免每次都要授权 options.no_reset = True # 关键配置:确保在测试后停止应用,释放资源 options.full_reset = False # 超时设置 options.new_command_timeout = 300 # 5分钟无新命令则超时 # 如果需要测试预装应用(如拨号盘),可以不指定app,而是指定appPackage和appActivity # options.app = '/path/to/your/app.apk' driver = webdriver.Remote('http://localhost:4723', options=options)

注意deviceName在Android上其实不是必须的,Appium主要靠udid来识别设备。但为了清晰,建议填写。更准确的做法是使用options.udid = ‘设备序列号’

3. 核心测试策略:从UI模拟到系统验证

语音通话自动化测试可以拆解为几个核心阶段:启动应用、拨号、验证通话建立、执行通话中操作、挂断、验证通话结束。每个阶段都需要UI操作和系统状态验证相结合。

3.1 启动应用与拨号操作

启动应用后,首先需要定位到拨号盘。这里就涉及到元素定位策略。对于系统拨号盘,不同手机厂商的UI差异很大,ID可能不同。

# 启动驱动后 try: # 1. 尝试通过ID定位拨号盘按钮(最常见) dialpad_btn = driver.find_element(AppiumBy.ID, ‘com.android.dialer:id/dialpad_fab’) dialpad_btn.click() time.sleep(1) # 等待动画 # 2. 如果ID定位失败,尝试其他策略 # 通过Accessibility ID(content-desc) # dialpad_btn = driver.find_element(AppiumBy.ACCESSIBILITY_ID, “拨号键盘”) # 通过XPath(灵活性高,但易碎) # dialpad_btn = driver.find_element(AppiumBy.XPATH, “//android.widget.ImageButton[@content-desc=‘拨号键盘’]”) # 3. 输入号码 number_mapping = { ‘1’: ‘com.android.dialer:id/one’, ‘2’: ‘com.android.dialer:id/two’, # ... 映射0-9, *, # } phone_number = ‘13800138000’ for digit in phone_number: if digit in number_mapping: driver.find_element(AppiumBy.ID, number_mapping[digit]).click() time.sleep(0.2) # 模拟人手输入间隔,避免过快 # 4. 点击拨打按钮 call_btn = driver.find_element(AppiumBy.ID, ‘com.android.dialer:id/dialpad_voice_call_button’) call_btn.click() except NoSuchElementException as e: print(f“UI元素定位失败: {e}”) # 备选方案:使用ADB模拟拨号盘输入和拨打 # adb shell am start -a android.intent.action.CALL -d tel:13800138000 # 但这会跳过UI,直接进入通话界面,适合纯逻辑测试。

实操心得:UI定位是自动化测试中最不稳定的环节。一定要为关键操作添加显式等待(WebDriverWait),而不是固定的sleep。同时,准备一个备选的ADB命令方案,当UI定位因系统升级或厂商定制失败时,脚本还能继续执行核心逻辑。

3.2 验证通话建立:UI与系统状态双保险

点击拨打后,如何判断电话真的打出去了?不能只靠UI上的“通话中”字样。

1. UI状态验证:

# 等待通话界面出现 WebDriverWait(driver, 10).until( EC.presence_of_element_located((AppiumBy.ID, ‘com.android.dialer:id/incall_end_call’)) ) # 检查是否有“通话中”等关键文本 page_source = driver.page_source assert “通话中” in page_source or “Dialing” in page_source or “Calling” in page_source

2. 系统状态验证(更可靠):这是关键步骤。通过ADB命令dumpsys telecom可以获取最权威的通话状态。

import subprocess def get_call_state_via_adb(): “””通过ADB获取当前通话状态””” result = subprocess.run([‘adb’, ‘shell’, ‘dumpsys’, ‘telecom’], capture_output=True, text=True) output = result.stdout # 在输出中查找关键信息 if ‘Call [ACTIVE’ in output: return ‘ACTIVE’ # 通话已接通 elif ‘Call [DIALING’ in output: return ‘DIALING’ # 正在拨号 elif ‘Call [DISCONNECTED’ in output: return ‘DISCONNECTED’ # 已挂断 else: return ‘IDLE’ # 空闲 # 在点击拨打后,循环检查状态,直到接通或超时 start_time = time.time() timeout = 30 while time.time() - start_time < timeout: state = get_call_state_via_adb() if state == ‘ACTIVE’: print(“通话已成功接通!”) break elif state == ‘DIALING’: print(“正在拨号中…”) time.sleep(2) else: print(f“当前状态: {state}”) time.sleep(1) else: raise AssertionError(“在指定时间内未接通电话”)

通过结合UI和系统层双重验证,我们的测试断言就非常坚实了。

3.3 通话中操作与挂断

通话建立后,我们可能需要测试一些功能,比如静音、免提、保持、拨号键盘等。

# 示例:点击免提按钮 speaker_btn = driver.find_element(AppiumBy.ID, ‘com.android.dialer:id/incall_speaker_button’) speaker_btn.click() time.sleep(1) # 可以再次通过dumpsys audio检查音频路由是否切换到扬声器 # adb shell dumpsys audio | grep “Devices:” # 挂断电话 end_call_btn = driver.find_element(AppiumBy.ID, ‘com.android.dialer:id/incall_end_call’) end_call_btn.click()

挂断后,同样需要进行验证:

# 等待通话界面消失,回到拨号盘或主界面 WebDriverWait(driver, 10).until( EC.presence_of_element_located((AppiumBy.ID, ‘com.android.dialer:id/dialpad_fab’)) ) # 系统状态验证 time.sleep(2) # 给系统一点处理时间 final_state = get_call_state_via_adb() assert final_state == ‘IDLE’, f“通话未正确结束,当前状态: {final_state}”

4. 进阶实战:模拟复杂场景与音频验证

基础的拨打通话流程自动化后,我们可以挑战更复杂的场景,这些才是体现自动化价值的所在。

4.1 模拟来电接听测试

测试接听功能,需要另一台设备或一个可以发起呼叫的工具。如果没有真机,可以借助一些模拟工具,但更可靠的方法是使用ADB模拟来电

Android提供了一个隐藏的测试命令来模拟来电:

adb shell am start -a android.intent.action.CALL -d tel:10086

但这其实是去电。模拟来电需要更底层的操作,通常需要系统签名权限,在普通App测试中很难实现。一个变通的方法是:

  1. 使用两台真机:一台作为测试机(A),一台作为呼叫机(B)。编写脚本控制B机通过ADB或另一套Appium驱动拨打A机的号码。
  2. 使用GSM模拟器或测试卡:在实验室环境下,使用专门的通信测试仪(如Anritsu、Keysight的设备)可以精确模拟网络信令,触发被测手机的来电。这是最专业的方式。
  3. 测试内建功能:如果你的应用是微信、Skype这类VoIP应用,来电本质上是网络信令。你可以在测试环境中,让服务端或另一客户端主动向被测客户端发起呼叫邀请。

4.2 弱网与网络切换模拟

语音通话质量对网络非常敏感。我们可以使用工具模拟弱网环境。

  • 使用Android模拟器的网络限制:如果你用的是模拟器,可以在启动时或通过ADB设置网络速度和延迟。
    adb shell svc data disable # 关闭蜂窝数据 adb shell svc wifi enable # 开启Wi-Fi # 使用NetEm(需要内核支持)模拟延迟和丢包,但这通常需要root权限
  • 使用系统开发者选项:在手机开发者选项中有“网络链接调节”功能,可以模拟2G、3G、高延迟网络等。可以通过ADB打开此界面,但自动设置参数比较困难。
  • 使用硬件网络损伤仪:在专业的测试实验室,这是标准配置。
  • 使用代理软件(如Charles、Fiddler):对于VoIP应用,其信令和媒体流走的是IP网络。可以在PC上设置代理,让手机流量经过代理,然后在代理软件上设置带宽限制、丢包率、延迟。这是对VoIP应用进行弱网测试最实用且成本较低的方法。

测试脚本需要在通话建立后,动态切换网络,并同时检查:

  1. UI上是否有网络状态提示(如“网络质量不佳”)。
  2. 通话是否异常中断。
  3. 通过获取通话日志(adb logcat | grep -i audio或应用自身的日志),分析是否有大量的音频包重传或解码错误。

4.3 音频质量的基础验证

完全自动化评估音频质量(如MOS分)需要专业的音频分析设备和算法,但我们可以做一些基础检查:

  1. 音频路径验证:确保通话时音频从正确的设备(听筒、扬声器、蓝牙耳机)输出/输入。可以通过adb shell dumpsys audio命令观察Selected output deviceActive input device
  2. 静音功能验证:点击静音按钮后,通过ADB录制一段系统音频(需要root权限),分析音频能量是否接近为零。
    adb shell screenrecord --audio-source=voice-communication /sdcard/test.aac # 将文件拉取到电脑,用音频分析工具(如sox, audacity)查看波形
  3. 双工通话测试:即双方同时说话。可以编写脚本,让两端设备在通话中按特定模式播放测试音频(如正弦波),然后在另一端录制并分析,检查是否有严重的剪裁或失真。这需要精确的时间同步,实现起来比较复杂,通常借助专业测试工具。

对于大多数业务测试,“音频是否正常”可以通过一个简单的“回声测试”来验证:在A端播放一段已知的音频文件(通过媒体音量),检查B端是否能录制到清晰可辨的相同音频。这可以通过在测试设备上安装一个简单的音频播放/录制App,并用Appium控制其配合主测试应用来完成。

5. 脚本优化与框架整合

单个测试用例跑通后,我们需要考虑如何将其工程化,融入团队的持续集成流程。

5.1 使用Page Object模式封装

将拨号盘、通话界面等抽象成Page类,使脚本更易维护。

class DialpadPage: def __init__(self, driver): self.driver = driver self.dialpad_fab = (AppiumBy.ID, ‘com.android.dialer:id/dialpad_fab’) self.digit_keys = {str(i): f‘com.android.dialer:id/{[“zero”,“one”,“two”,“three”,“four”,“five”,“six”,“seven”,“eight”,“nine”][i]}’ for i in range(10)} self.call_button = (AppiumBy.ID, ‘com.android.dialer:id/dialpad_voice_call_button’) def open_dialpad(self): WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable(self.dialpad_fab)).click() def dial_number(self, number): for digit in number: if digit in self.digit_keys: key_locator = (AppiumBy.ID, self.digit_keys[digit]) WebDriverWait(self.driver, 5).until(EC.element_to_be_clickable(key_locator)).click() time.sleep(0.1) def make_call(self): WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable(self.call_button)).click() class CallScreenPage: # … 定义通话界面的元素和操作

5.2 测试数据与配置外部化

将设备信息、等待超时、电话号码等配置抽取到配置文件(如config.yaml.env)中。

# config.yaml devices: - udid: emulator-5554 platform_version: ‘14’ call_number: ‘13800138000’ - udid: ‘ABCDEF012345’ platform_version: ‘13’ call_number: ‘10086’ appium: server_url: ‘http://localhost:4723’ implicit_wait: 10

5.3 集成到CI/CD流水线

在Jenkins、GitLab CI等工具中,可以这样安排任务:

  1. 准备阶段:CI Agent连接并检查测试设备状态(adb devices)。
  2. 构建阶段:安装测试依赖(Appium Python客户端等)。
  3. 测试阶段
    • 启动Appium服务器。
    • 并行执行测试套件(使用pytest-xdist)。
    • 收集测试过程中的屏幕录像(adb screenrecord)、Logcat日志和Appium日志。
  4. 报告阶段:使用pytest-htmlAllure生成美观的测试报告,附上失败用例的截图和日志。
  5. 清理阶段:无论成功失败,都强制关闭Appium会话,释放设备。

5.4 并行测试与资源管理

当有多台设备时,可以使用pytest+appium-python-client+threadingmultiprocessing实现并行。更优雅的方式是使用Selenium Grid的思路,搭建Appium Grid。一个Hub管理多个注册了不同设备的Node,测试脚本只需将Remote命令指向Hub,由Hub分配可用的设备。

6. 常见问题排查与实战技巧

在实际操作中,你会遇到各种各样的问题。这里记录了几个最典型的“坑”和解决方法。

6.1 元素定位失败问题

这是最高频的问题。

  • 现象NoSuchElementException
  • 排查
    1. 检查上下文(Context):混合应用(Hybrid App)或Flutter应用中,需要在Native和Webview上下文间切换。使用driver.contextsdriver.switch_to.context
    2. 使用更稳定的定位器:优先使用IDACCESSIBILITY_ID。如果都没有,尝试相对XPath,避免使用绝对路径和可能变化的索引。
    3. 添加智能等待:永远不要只用time.sleep。使用WebDriverWait配合expected_conditions
      from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC element = WebDriverWait(driver, 15).until( EC.presence_of_element_located((AppiumBy.ID, “some_id”)) )
    4. 使用Appium Desktop或Inspect工具:重新检查元素属性,确认定位器没有写错。注意有些元素的ID在应用不同版本间可能会变。

6.2 权限弹窗与系统对话框干扰

  • 现象:脚本执行被“是否允许拨打电话?”等弹窗打断。
  • 解决
    1. 前置授权:如前所述,在测试开始前通过adb shell pm grant命令授予所有所需权限。
    2. 自动处理弹窗:如果无法前置授权,可以编写代码检测并点击弹窗。但不同系统弹窗的定位器差异极大,非常脆弱。一个相对通用的方法是使用ADB模拟按下“允许”键:
      # 在可能出现弹窗的操作后,尝试按下“允许” subprocess.run([‘adb’, ‘shell’, ‘input’, ‘keyevent’, ‘KEYCODE_ENTER’]) # 有时是回车键 # 或者更暴力地,点击屏幕固定坐标(不推荐,兼容性差)
    3. 使用autoGrantPermissionsCapability:在UiAutomator2Options中设置options.auto_grant_permissions = True,Appium会自动点击授权弹窗。但并非100%有效。

6.3 设备兼容性与稳定性

  • 现象:在一台设备上运行良好的脚本,在另一台设备上失败。
  • 解决
    1. 抽象设备操作:将对设备的直接操作(如ADB命令)封装起来,根据设备型号或系统版本进行适配。
    2. 准备多套定位器:为不同厂商的ROM准备不同的元素定位器映射表。
    3. 增加重试机制:对于不稳定的操作(如网络请求、页面跳转),使用重试装饰器。
      from retrying import retry @retry(stop_max_attempt_number=3, wait_fixed=2000) def unstable_operation(): # 你的操作代码 pass
    4. 定期重启设备/Appium Server:长时间运行测试后,设备可能会卡顿,Appium Server也可能内存泄漏。在CI任务中,每次任务开始前重启设备是个好习惯。

6.4 通话状态检测的延迟与误判

  • 现象dumpsys telecom命令返回状态有延迟,导致脚本误判。
  • 解决
    1. 轮询结合超时:如前文代码所示,不要只检查一次。设置一个合理的总超时时间(如30秒),在超时前循环检查。
    2. 增加缓冲时间:在关键状态转换操作(如点击挂断)后,等待2-3秒再检查系统状态,给系统处理留出时间。
    3. 多指标综合判断:不要只依赖dumpsys telecom。同时检查UI元素是否存在、Logcat中是否有特定的通话状态日志(adb logcat | grep -i “call state”),进行综合判断。

6.5 性能与资源泄漏

  • 现象:长时间运行大量测试用例后,设备变慢,脚本失败率升高。
  • 解决
    1. 务必在finally块或teardown方法中调用driver.quit():确保每次测试会话结束后,释放Appium与设备之间的连接资源。
    2. 清理后台进程:测试开始前,通过adb shell am force-stop <package_name>清理被测应用和其他可能干扰的应用。
    3. 监控设备资源:在CI脚本中,可以定期运行adb shell topadb shell dumpsys meminfo来监控CPU和内存使用情况,发现异常及时报警。

语音通话的自动化测试,难点不在于拨号点击,而在于对“状态”的精确感知和控制。它要求测试开发人员不仅懂UI自动化,还要了解一点移动操作系统(特别是Android)的通信子系统知识,并善于利用ADB这个强大的命令行工具。将Appium的UI操作与ADB的系统级命令结合起来,才能构建出稳定、可靠的语音通话自动化测试方案。这套思路同样可以扩展到短信、联系人、相机等其他需要与系统深度交互的功能测试中。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询