Appium自动化测试实战:语音通话功能从环境搭建到质量验证

1. 项目概述与核心价值

最近在做一个涉及实时语音通话功能的App测试项目,团队里的小伙伴们被手动测试折腾得够呛。想象一下,测试人员每天要做的就是:拿起A手机拨号,拿起B手机接听,对着话筒“喂喂喂”,然后挂断,再换一组号码、换一种网络环境重复。不仅枯燥,而且通话时长、音质、延迟这些关键指标全靠耳朵听、手动记,效率低不说,数据还很不准。更头疼的是,一旦要测多设备并发或者弱网下的异常场景,手动测试几乎无法覆盖。这种痛点,我相信很多做社交、会议、客服类App的测试同行都深有体会。

正是在这种背景下,我们决定用Appium把语音通话的测试给自动化了。这不仅仅是把“点击拨号按钮”这个动作自动化那么简单,它的核心价值在于构建一个可重复、可量化、可扩展的测试闭环。通过脚本,我们可以精确模拟用户从发起呼叫、建立连接到结束通话的全流程,并能自动采集通话过程中的关键数据,比如端到端延迟、音频丢包率,甚至是利用简单的语音识别来校验通话内容是否正确。这样一来,回归测试的覆盖率上去了,夜间可以跑大量的压力测试场景,释放了人力,更重要的是,测试结果从“感觉还行”变成了“延迟小于200ms,丢包率0.5%”这样的客观数据,为产品质量提供了坚实保障。

2. 环境搭建与工具链选型

工欲善其事,必先利其器。在开始写自动化脚本之前,一个稳定、兼容的环境是基石。这里我结合自己的踩坑经验,把环境搭建的要点和背后的“为什么”讲清楚。

2.1 核心工具安装与版本协同

首先明确我们的技术栈:Appium Server 作为测试引擎,Appium Client(Python版)作为脚本编写端,Android SDK/ADB 用于与设备通信,一部安卓真机或模拟器作为被测对象。

  1. Node.js与Appium Server:Appium Server是基于Node.js的,所以第一步是安装Node.js。这里有个关键点,不要追求最新版本。我曾因为用了太新的Node.js导致与某个版本的Appium不兼容,出现各种诡异的启动错误。建议使用Node.js的LTS(长期支持)版本,比如18.x。安装完成后,通过npm安装Appium Server:npm install -g appium。安装后,可以通过appium -vappium driver list来验证安装,并确保uiautomator2这个驱动(用于安卓)已安装。

  2. Python与Appium Client:Python环境推荐使用3.8或3.9,同样以稳定为主。使用pip安装必要的包:pip install Appium-Python-Client。这个库是我们编写脚本时,用来向Appium Server发送指令的桥梁。

  3. Android SDK与ADB:这是与安卓设备通信的核心。建议直接下载Android Studio,它自带SDK Manager,可以方便地安装所需的Platform-Tools(包含ADB)和系统镜像。安装后,请务必将SDK的platform-tools目录添加到系统的PATH环境变量中。验证方式是在命令行输入adb version,能显示版本号即成功。这里有个大坑:如果你电脑上同时存在多个ADB(比如某些手机助手自带的),可能会产生冲突,导致设备无法识别。解决方法是统一使用Android SDK里的ADB,并确保它在PATH中的顺序最靠前。

  4. 被测设备准备:真机优于模拟器。真机请开启“开发者选项”和“USB调试”。模拟器可以使用Android Studio自带的AVD Manager创建。特别注意:无论是真机还是模拟器,建议在开发者选项里,将“窗口动画缩放”、“过渡动画缩放”、“动画程序时长缩放”这三项都设置为“关闭动画”。这能显著减少脚本执行中因等待动画结束而导致的超时失败。

2.2 必备辅助工具:元素定位的“眼睛”

写UI自动化脚本,70%的时间可能花在元素定位上。光靠猜和试是不行的,你需要好用的侦查工具。

  1. Appium Inspector:这是Appium官方提供的图形化元素定位工具。新版Appium Server(2.0+)通常自带。它的作用是连接到你的设备,实时显示当前界面的UI层级树,让你可以点击查看任意元素的属性(如resource-id, text, class, content-desc等)。在编写定位代码前,先用Inspector探查清楚,事半功倍。使用技巧:启动Inspector时,需要的desired_capabilities配置几乎和你的测试脚本一致,这意味着你可以直接把脚本里的配置参数复制过来用。

  2. UIAutomatorViewer:这是Android SDK自带的老牌工具,位于SDK的tools/bin目录下。在某些情况下,特别是对于老旧设备或Appium Inspector无法正常连接时,它可能更稳定。它的功能与Appium Inspector类似。

  3. ADB命令行:这是我们的“瑞士军刀”。除了安装应用(adb install)、启动应用(adb shell am start),在语音通话测试中,它有大用场。例如,我们可以用adb shell dumpsys telecom来获取通话状态,用adb shell logcat来抓取应用和系统的音频相关日志,甚至可以用adb shell input keyevent来模拟物理按键(如挂断键)。熟练掌握ADB,能让你的自动化脚本能力提升一个维度。

注意:环境搭建完成后,务必做一个“端到端”的连通性测试:启动Appium Server -> 使用ADB确认设备已连接 -> 写一个最简单的打开拨号盘的脚本并运行。确保这一步通了,再开始后续复杂的业务逻辑开发,否则调试起来会非常痛苦。

3. 权限处理与拨号核心逻辑实现

语音通话功能涉及敏感的硬件(麦克风、听筒)和系统权限(打电话),自动化脚本必须妥善处理这些权限问题,并稳定地模拟用户拨号操作。

3.1 自动化权限授予策略

安卓应用在首次使用麦克风或电话功能时,会弹出系统权限弹窗。如果脚本不处理,就会卡在这里失败。我们有几种策略:

  1. Desired Capabilities 预授权:在初始化驱动时,通过desired_capabilities参数让Appium自动处理。这是最推荐的方式。

    desired_caps = { 'platformName': 'Android', 'deviceName': 'your_device_name', 'appPackage': 'com.yourcompany.dialer', 'appActivity': '.MainActivity', 'autoGrantPermissions': True, # 自动授予所有弹窗权限 # 或者更精细的控制 # 'autoAcceptAlerts': True, # 自动接受所有弹窗(包括权限和非权限) }

    autoGrantPermissions: True会让Appium自动点击权限弹窗的“允许”按钮。但要注意,有些应用在非首次启动时也会因权限问题弹窗,这个参数同样有效。

  2. ADB命令预授权:在测试开始前,通过ADB命令直接授予权限。这种方式更底层,不依赖Appium。

    adb shell pm grant com.yourcompany.dialer android.permission.RECORD_AUDIO adb shell pm grant com.yourcompany.dialer android.permission.CALL_PHONE adb shell pm grant com.yourcompany.dialer android.permission.MODIFY_AUDIO_SETTINGS

    你可以把这些命令写在脚本的setUp方法里。好处是绝对可靠,缺点是需要提前知道应用包名和具体的权限名。

  3. 脚本动态处理:如果上述方法失效,或者你只想在特定时机授权,可以在脚本中检测并点击权限弹窗。但这需要定位弹窗元素,稳定性较差,作为保底方案。实操心得:对于内部测试包,我强烈推荐方法2(ADB预授权),干净彻底。对于无法预知权限场景的情况,方法1(autoGrantPermissions)是首选。务必在真机上测试权限处理逻辑,因为模拟器的权限行为可能与真机有差异。

3.2 拨号操作的元素定位与交互

拨号界面通常包含数字键盘、拨号按钮、联系人输入框等。定位这些元素是第一步。

  1. 使用Appium Inspector定位:打开拨号盘,用Inspector查看。理想的定位依据是resource-id(安卓)或accessibility id(iOS/安卓),因为它们是唯一且稳定的。例如,数字键“1”的id可能是com.android.dialer:id/one

  2. 编写稳健的拨号脚本:假设我们测试拨打号码“10086”。

    from appium import webdriver from appium.webdriver.common.appiumby import AppiumBy import time # ... desired_caps 配置 ... driver = webdriver.Remote('http://localhost:4723', desired_caps) time.sleep(2) # 等待应用启动稳定 try: # 1. 点击拨号盘按钮(如果不在默认页) # 先判断元素是否存在,再点击,增加容错 dial_pad_btn = driver.find_elements(AppiumBy.ID, 'com.android.dialer:id/dialpad_fab') if dial_pad_btn: dial_pad_btn[0].click() time.sleep(1) # 2. 输入号码:使用ID定位每个数字键 number_mapping = { '1': 'com.android.dialer:id/one', '0': 'com.android.dialer:id/zero', # ... 其他数字 } for digit in '10086': element_id = number_mapping.get(digit) if element_id: driver.find_element(AppiumBy.ID, element_id).click() time.sleep(0.2) # 短暂间隔,模拟人手速 else: print(f"Warning: No mapping for digit {digit}") # 3. 点击拨打按钮 call_button = driver.find_element(AppiumBy.ID, 'com.android.dialer:id/dialpad_floating_action_button') call_button.click() print("拨号指令已发送") # 此时,系统会跳转到通话界面 except Exception as e: print(f"拨号过程发生异常: {e}") # 这里可以添加截图逻辑,方便后期排查 driver.save_screenshot('dial_failed.png')

    关键点解析

    • find_elementsfind_elementfind_elements返回列表,即使找不到元素也是空列表,不会抛异常。用于先判断元素是否存在,非常适合处理界面状态不确定的情况。
    • 等待策略:直接使用time.sleep是最简单但不推荐的方式,因为它固定等待,浪费时间。在实际项目中,应该使用显式等待(WebDriverWait),让脚本在元素出现或可点击时才进行下一步,效率更高。
    • 异常处理与日志:一定要用try...except包裹核心操作,并记录详细的日志或截图。当脚本在CI/CD上夜间运行时,这些信息是定位问题的唯一线索。

4. 通话状态监控与断言机制

拨号成功只是开始,自动化测试需要验证通话是否真正建立、通话质量如何。这就需要我们监控通话状态,并设计断言(Assertion)来判定测试用例的成功与否。

4.1 利用ADB监控通话状态

UI上“正在通话”的界面可能因手机厂商定制而千差万别,通过UI元素判断不稳定。更可靠的方式是通过ADB查询系统底层的通话状态。

import subprocess def get_call_state_via_adb(device_id=''): """ 通过ADB命令获取当前通话状态。 返回:'IDLE'(空闲), 'RINGING'(响铃), 'OFFHOOK'(摘机,包括拨号、通话中) """ # 如果有多个设备,需要指定 -s 参数 device_prefix = f'-s {device_id} ' if device_id else '' cmd = f'adb {device_prefix}shell dumpsys telecom' try: result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=5) output = result.stdout # 在输出中查找关键行 if 'Call 1: DOWN' in output or 'Calls in call manager:' in output and 'num=0' in output: return 'IDLE' elif 'RINGING' in output: return 'RINGING' elif 'ACTIVE' in output or 'DIALING' in output: return 'OFFHOOK' else: # 如果解析失败,可以尝试更通用的方法:检查是否有进程在使用电话相关服务 cmd_check = f'adb {device_prefix}shell "ps | grep telephony"' result_check = subprocess.run(cmd_check, shell=True, capture_output=True, text=True) if result_check.stdout: return 'OFFHOOK' # 可能存在通话 return 'UNKNOWN' except subprocess.TimeoutExpired: print("ADB命令执行超时") return 'ERROR' except Exception as e: print(f"获取通话状态失败: {e}") return 'ERROR' # 在拨号后,使用该函数进行验证 call_button.click() print("拨号指令已发送,等待通话建立...") time.sleep(3) # 等待系统处理拨号请求 max_wait = 30 # 最大等待30秒 start_time = time.time() while time.time() - start_time < max_wait: state = get_call_state_via_adb() if state == 'OFFHOOK': print("通话已成功建立!") break elif state == 'IDLE': print("通话未接通或已挂断。") # 这里可以结合UI判断,是否是对方未接听等场景 break time.sleep(2) # 每2秒检查一次 else: print("错误:在指定时间内未检测到通话建立。") # 标记测试用例为失败

为什么用dumpsys telecom因为它是安卓系统Telecom框架的服务,所有通话(包括第三方VoIP应用,如果它们集成得当)的状态理论上都会在这里管理。相比解析UI,这种方法更接近事实本源,不受皮肤和主题影响。

4.2 设计多维度的断言条件

一个健壮的通话自动化测试用例,其断言不应该只有“接通了”这么简单。我们需要从多个维度验证:

  1. 状态断言:如上所述,核心断言是通话状态必须从DIALING(拨号中)转变为ACTIVE(通话中)。这是最基本的要求。

  2. 时长断言:测试通话保持功能。脚本可以在通话建立后,等待一个预设的时长(例如30秒),然后再次检查通话状态是否仍为ACTIVE。如果中途断线,测试失败。

  3. UI辅助断言:虽然不作为主要依据,但可以辅助验证。例如,检查通话界面是否显示了正确的联系人姓名或号码(通过定位对应的TextView)。这可以验证呼叫的目标是否正确。

  4. 音频通道断言(进阶):通过ADB命令adb shell dumpsys audio可以查看当前的音频路由(AudioFocus)。我们可以检查在通话建立后,音频焦点是否被我们的应用或电话应用获取,并且路由到了听筒或扬声器(取决于测试场景)。这能验证音频硬件是否被正确调用。

一个综合断言逻辑的示例片段:

# 假设通话已建立,状态为 ACTIVE assert get_call_state_via_adb() == 'OFFHOOK', "主断言失败:通话未处于接通状态" # 等待并断言通话能持续一段时间 hold_duration = 10 print(f"开始保持通话 {hold_duration} 秒...") time.sleep(hold_duration) assert get_call_state_via_adb() == 'OFFHOOK', f"通话未能保持 {hold_duration} 秒" # (可选)UI断言:检查通话界面显示的正确号码 if driver.find_elements(AppiumBy.ID, 'com.android.dialer:id/contact_name_or_number'): displayed_number = driver.find_element(AppiumBy.ID, 'com.android.dialer:id/contact_name_or_number').text assert '10086' in displayed_number, f"显示号码不匹配,期望包含10086,实际为{displayed_number}" print("UI号码显示正确。")

5. 挂断操作与测试数据清理

测试用例需要有始有终,挂断操作和清理环境是保证测试可重复执行的关键。

5.1 多种挂断方式及其适用场景

挂断电话,至少有三种可靠的自动化实现方式,各有优劣:

  1. 通过UI元素点击挂断按钮:这是最接近用户操作的方式。

    # 定位通话界面的大红色挂断按钮 end_call_button = driver.find_element(AppiumBy.ID, 'com.android.dialer:id/incall_end_call') end_call_button.click() print("通过UI点击挂断。")

    优点:完全模拟用户。缺点:按钮的ID可能因ROM不同而变化,稳定性最差。

  2. 发送ADB输入事件模拟按下“挂断键”:安卓系统有一个虚拟的“结束通话”键值。

    import subprocess subprocess.run('adb shell input keyevent KEYCODE_ENDCALL', shell=True) print("通过ADB模拟挂断键挂断。")

    优点:通用性强,几乎在所有安卓设备上都有效。缺点:这是一个全局事件,如果当时有其他应用在前台并响应了这个键值,可能会产生意外效果。

  3. 使用Appium的press_keycode方法:原理同ADB命令,但通过Appium驱动执行。

    from appium.webdriver.extensions.android.native_key import AndroidKey driver.press_keycode(AndroidKey.ENDCALL) print("通过Appium发送挂断键码。")

    优点:比ADB命令更“优雅”,属于WebDriver协议的一部分。缺点:需要导入特定的Keycode枚举。

实操心得:在实际项目中,我通常会采用**“UI优先,ADB兜底”**的策略。先尝试用UI方式挂断,如果找不到元素(可能界面已变化),则捕获异常,转而执行ADB命令挂断。这样既保证了在正常情况下的模拟真实性,又有了保底的可靠性。

5.2 通话记录清理与测试隔离

自动化测试,尤其是会往服务器或本地数据库写入数据的测试,必须考虑数据清理,防止测试数据堆积影响后续测试或产生脏数据。

  1. 清理本地通话记录:对于手机自带的拨号应用,通话记录会保存在本地。我们可以通过ADB命令删除这些记录,或者更简单地,在测试开始前/后,直接清除拨号应用的数据。

    # 方法1:通过ADB清除应用数据(相当于恢复出厂设置,会清空所有数据,包括设置) subprocess.run('adb shell pm clear com.android.dialer', shell=True) # 注意:这非常暴力,会清空所有数据,包括已保存的设置。慎用! # 方法2:更精细地通过Content Provider删除(需要知道具体URI,较复杂) # adb shell content delete --uri content://call_log/calls
  2. 使用隔离的测试账号/号码:如果测试的是像微信语音这样的网络通话,强烈建议使用专门为此自动化测试创建的、隔离的测试账号。这些账号只用于自动化测试,不与真实业务数据混淆。脚本应在setUp中登录测试账号A,在另一台设备或模拟器上登录测试账号B。

  3. 脚本层面的清理:在测试用例的tearDown方法中,无论测试成功还是失败,都应执行清理操作。例如,强制挂断所有通话(通过多次发送KEYCODE_ENDCALL),返回到应用主界面,甚至重启应用,为下一个测试用例提供一个干净的环境。

    def tearDown(self): # 确保通话被挂断 try: self.driver.press_keycode(AndroidKey.ENDCALL) except: pass # 强制回到主屏幕 self.driver.press_keycode(AndroidKey.HOME) # 如果应用进程还在,可以终止它 subprocess.run(f'adb shell am force-stop {self.desired_caps["appPackage"]}', shell=True) # 最后退出驱动 if self.driver: self.driver.quit()

6. 进阶:音频质量检测与网络模拟

基础的接通/挂断自动化只是第一步。要真正评估语音通话功能的质量,我们需要向“质量保障”迈进,即自动化地评估通话的音频质量。

6.1 利用ADB进行简单的音频活动检测

虽然无法直接通过Appium分析音频流内容,但我们可以通过ADB间接判断音频系统是否在工作。

  1. 检查音频进程:通话建立后,系统会有相关的音频处理进程(如mediaserver,audioserver)活跃,或者电话应用本身会持有音频焦点。

    adb shell ps | grep -E ‘(mediaserver|audioserver|audio)’

    通过对比通话前后这些进程的状态或资源占用,可以侧面验证音频通道是否被激活。

  2. 监控Logcat中的音频相关日志:安卓系统的音频子系统会输出大量日志。我们可以过滤关键字来观察。

    import subprocess # 开始通话 # ... # 同时,在另一个线程或进程中开始收集日志 log_process = subprocess.Popen( ['adb', 'logcat', '-s', 'AudioTrack:V', 'AudioRecord:V', 'AudioFlinger:V'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True ) # 运行一段时间通话测试... # 然后终止日志收集 log_process.terminate() stdout, _ = log_process.communicate() # 分析stdout中是否有音频数据开始/停止、缓冲区变化等关键信息 if "start()" in stdout and "AudioTrack" in stdout: print("检测到音频播放活动。") if "start()" in stdout and "AudioRecord" in stdout: print("检测到音频录制活动。")

    这种方法需要你对安卓音频日志有一定的了解,用于判断音频链路是否打通非常有效。

6.2 集成第三方SDK进行音频分析(概念)

对于要求更高的测试,比如需要测量端到端延迟、丢包率、MOS分,可以考虑在测试端集成音频分析SDK。但这通常超出了UI自动化的范畴,需要开发专门的测试桩(Test Stub)或使用云测平台的服务。

一种可行的思路

  1. 在两端(主叫和被叫)的测试环境中,预装一个自定义的“音频分析助手”App。
  2. 通话建立后,自动化脚本通过ADB或广播,通知这个助手App开始播放一段标准的测试音频(例如一段特定的正弦波或语音)。
  3. 另一端的助手App同时开始录音。
  4. 通话结束后,助手App分析录音文件,通过算法(如互相关算法)计算出音频从发送到接收的延迟,并分析音频失真程度。
  5. 自动化脚本再从助手App拉取分析结果,作为测试断言的一部分。

这实现起来比较复杂,属于专项测试的范畴。但对于核心的语音通话产品,这种投入是值得的。

6.3 使用网络模拟工具测试弱网场景

语音通话质量与网络状况强相关。自动化测试必须覆盖弱网、高延迟、高丢包等场景。

  1. 工具选择

    • Charles/ Fiddler:适合模拟手机HTTP/HTTPS代理下的网络状况,但对纯TCP/UDP的语音流(如WebRTC)可能不直接生效。
    • Android Emulator 自带网络模拟:Android Studio的模拟器可以直接设置网络速度和延迟,非常方便。
    • 硬件设备+网络损伤仪:最真实但成本高,用于实验室环境。
    • 软件方案:netem(Linux) 或Network Link Conditioner(macOS):如果你使用电脑开热点给手机,可以在电脑端用这些工具模拟网络损伤。
  2. 在自动化脚本中集成网络模拟:以使用netem为例,你可以在测试用例的setUp中执行Shell命令来设置网络规则,在tearDown中清除规则。

    import subprocess class TestVoiceCall: def setUp(self): # 设置200ms延迟,1%丢包的网络环境 subprocess.run('sudo tc qdisc add dev eth0 root netem delay 200ms loss 1%', shell=True) # ... 其他初始化 def test_call_in_poor_network(self): # 执行通话测试 # 断言条件可以放宽,例如允许更长的连接建立时间 pass def tearDown(self): # 清除网络模拟规则 subprocess.run('sudo tc qdisc del dev eth0 root', shell=True) # ... 其他清理

    重要提示:网络模拟会影响整个设备的网络,请确保在独立的测试机或模拟器上进行,避免影响开发环境。

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

即使环境搭建完美,脚本逻辑严谨,在实际运行中还是会遇到各种“坑”。下面是我总结的一些高频问题和解决思路。

7.1 元素定位失败与动态内容处理

这是UI自动化最常见的问题。

  • 问题:脚本报错NoSuchElementException
  • 排查
    1. 确认界面:首先通过driver.get_screenshot_as_file('debug.png')截图,或者用driver.page_source打印当前页面XML源码,确认是否真的跳转到了你期望的界面。
    2. 检查定位符:用Appium Inspector重新连接设备,查看目标元素的属性是否变了。特别是resource-id,有些应用在不同版本或不同状态下会动态生成。
    3. 等待时机:元素还没加载出来你就去点击了。永远不要只用time.sleep。改用显式等待。
      from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from appium.webdriver.common.appiumby import AppiumBy # 等待最多10秒,直到拨号按钮出现并可点击 wait = WebDriverWait(driver, 10) dial_button = wait.until(EC.element_to_be_clickable((AppiumBy.ID, 'com.android.dialer:id/dialpad_fab'))) dial_button.click()
    4. 备用定位策略:如果ID不稳定,尝试用其他属性组合定位,比如XPath(但尽量少用,性能差且易变)、accessibility id(对于有内容的元素)、class name配合text等。
    5. 处理动态ID:如果ID是动态的(包含时间戳或随机数),尝试用XPathcontainsstarts-with等函数进行模糊匹配。
      # 例如,ID是‘button_123456’,其中数字部分动态变化 driver.find_element(AppiumBy.XPATH, "//*[contains(@resource-id, 'button_')]")

7.2 权限弹窗与系统弹窗干扰

  • 问题:测试被突如其来的“是否允许应用访问通讯录?”、“是否允许录音?”等弹窗打断。
  • 解决
    • 首选:如前所述,在desired_capabilities中设置'autoGrantPermissions': True'autoAcceptAlerts': True
    • 备选:如果上述无效,可以写一个通用的弹窗处理函数,在每次操作后检查并处理。
      def handle_alert_if_present(driver): try: # 尝试查找常见的允许/确定按钮,可能需要适配不同系统 allow_btn = driver.find_element(AppiumBy.ID, 'com.android.packageinstaller:id/permission_allow_button') # 或者用更通用的定位:包含‘允许’或‘ALLOW’文字的按钮 # allow_btn = driver.find_element(AppiumBy.XPATH, "//*[contains(@text, '允许') or contains(@text, 'ALLOW')]") allow_btn.click() return True except: return False # 在关键操作后调用 dial_button.click() time.sleep(1) # 给弹窗一点时间弹出 handle_alert_if_present(driver)

7.3 跨设备同步与时序问题

  • 问题:主叫脚本已经拨号,但被叫脚本还没准备好接听,导致呼叫超时失败。
  • 解决
    • 状态同步:不要依赖固定的sleep。主叫拨号后,可以轮询查询自身状态是否为DIALINGOFFHOOK,确认拨号请求已发出。被叫端则轮询检查来电状态(RINGING)。
    • 使用简单的网络同步:如果两台设备在同一个网络,可以搭建一个极简的HTTP服务作为“协调器”。主叫拨号后,向协调器发送“我已拨号”的信号;被叫端轮询协调器,收到信号后再开始执行接听检查。这比单纯靠时间同步要可靠得多。
    • 增加重试和超时机制:任何可能失败的操作(如点击按钮、检查状态)都应该包裹在重试逻辑中。
      from selenium.common.exceptions import NoSuchElementException, TimeoutException import time def click_with_retry(driver, locator, max_attempts=3): for attempt in range(max_attempts): try: element = driver.find_element(*locator) element.click() return True except (NoSuchElementException, Exception) as e: if attempt == max_attempts - 1: raise e print(f"点击失败,第{attempt+1}次重试...") time.sleep(2) return False # 使用方式 click_with_retry(driver, (AppiumBy.ID, 'com.android.dialer:id/some_button'))

7.4 性能优化与稳定性提升

  • 减少对UI的依赖:能用ADB命令完成的操作,优先使用ADB。例如,挂断电话用KEYCODE_ENDCALL比找UI按钮稳定得多。启动应用也可以用adb shell am start
  • 使用resetOnSessionStartOnly:在desired_capabilities中设置'resetOnSessionStartOnly': True。这会让Appium在第一次启动应用时执行重置(如清除数据),但在同一个会话内的后续测试中不再重置。这可以节省大量时间,特别是你的测试用例需要依赖上一个用例的状态时(需谨慎设计用例顺序)。
  • 合理管理会话:对于一组相关的测试用例,尽量复用同一个driver会话,而不是每个用例都重新启动应用。在setUpClasstearDownClass中管理驱动生命周期。

把这些技巧融入到你的脚本和测试框架中,能极大提升自动化测试的稳定性和执行效率,让它从“偶尔能跑通”变成“持续稳定运行”的可靠质量守护环节。