1. 项目概述与核心价值
最近在做一个涉及实时语音通话功能的App测试项目,团队里的小伙伴们被手动测试折腾得够呛。想象一下,测试人员每天要做的就是:拿起A手机拨号,拿起B手机接听,对着话筒“喂喂喂”,然后挂断,再换一组号码、换一种网络环境重复。不仅枯燥,而且通话时长、音质、延迟这些关键指标全靠耳朵听、手动记,效率低不说,数据还很不准。更头疼的是,一旦要测多设备并发或者弱网下的异常场景,手动测试几乎无法覆盖。这种痛点,我相信很多做社交、会议、客服类App的测试同行都深有体会。
正是在这种背景下,我们决定用Appium把语音通话的测试给自动化了。这不仅仅是把“点击拨号按钮”这个动作自动化那么简单,它的核心价值在于构建一个可重复、可量化、可扩展的测试闭环。通过脚本,我们可以精确模拟用户从发起呼叫、建立连接到结束通话的全流程,并能自动采集通话过程中的关键数据,比如端到端延迟、音频丢包率,甚至是利用简单的语音识别来校验通话内容是否正确。这样一来,回归测试的覆盖率上去了,夜间可以跑大量的压力测试场景,释放了人力,更重要的是,测试结果从“感觉还行”变成了“延迟小于200ms,丢包率0.5%”这样的客观数据,为产品质量提供了坚实保障。
2. 环境搭建与工具链选型
工欲善其事,必先利其器。在开始写自动化脚本之前,一个稳定、兼容的环境是基石。这里我结合自己的踩坑经验,把环境搭建的要点和背后的“为什么”讲清楚。
2.1 核心工具安装与版本协同
首先明确我们的技术栈:Appium Server 作为测试引擎,Appium Client(Python版)作为脚本编写端,Android SDK/ADB 用于与设备通信,一部安卓真机或模拟器作为被测对象。
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 -v和appium driver list来验证安装,并确保uiautomator2这个驱动(用于安卓)已安装。Python与Appium Client:Python环境推荐使用3.8或3.9,同样以稳定为主。使用pip安装必要的包:
pip install Appium-Python-Client。这个库是我们编写脚本时,用来向Appium Server发送指令的桥梁。Android SDK与ADB:这是与安卓设备通信的核心。建议直接下载Android Studio,它自带SDK Manager,可以方便地安装所需的Platform-Tools(包含ADB)和系统镜像。安装后,请务必将SDK的
platform-tools目录添加到系统的PATH环境变量中。验证方式是在命令行输入adb version,能显示版本号即成功。这里有个大坑:如果你电脑上同时存在多个ADB(比如某些手机助手自带的),可能会产生冲突,导致设备无法识别。解决方法是统一使用Android SDK里的ADB,并确保它在PATH中的顺序最靠前。被测设备准备:真机优于模拟器。真机请开启“开发者选项”和“USB调试”。模拟器可以使用Android Studio自带的AVD Manager创建。特别注意:无论是真机还是模拟器,建议在开发者选项里,将“窗口动画缩放”、“过渡动画缩放”、“动画程序时长缩放”这三项都设置为“关闭动画”。这能显著减少脚本执行中因等待动画结束而导致的超时失败。
2.2 必备辅助工具:元素定位的“眼睛”
写UI自动化脚本,70%的时间可能花在元素定位上。光靠猜和试是不行的,你需要好用的侦查工具。
Appium Inspector:这是Appium官方提供的图形化元素定位工具。新版Appium Server(2.0+)通常自带。它的作用是连接到你的设备,实时显示当前界面的UI层级树,让你可以点击查看任意元素的属性(如resource-id, text, class, content-desc等)。在编写定位代码前,先用Inspector探查清楚,事半功倍。使用技巧:启动Inspector时,需要的
desired_capabilities配置几乎和你的测试脚本一致,这意味着你可以直接把脚本里的配置参数复制过来用。UIAutomatorViewer:这是Android SDK自带的老牌工具,位于SDK的
tools/bin目录下。在某些情况下,特别是对于老旧设备或Appium Inspector无法正常连接时,它可能更稳定。它的功能与Appium Inspector类似。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 自动化权限授予策略
安卓应用在首次使用麦克风或电话功能时,会弹出系统权限弹窗。如果脚本不处理,就会卡在这里失败。我们有几种策略:
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自动点击权限弹窗的“允许”按钮。但要注意,有些应用在非首次启动时也会因权限问题弹窗,这个参数同样有效。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方法里。好处是绝对可靠,缺点是需要提前知道应用包名和具体的权限名。脚本动态处理:如果上述方法失效,或者你只想在特定时机授权,可以在脚本中检测并点击权限弹窗。但这需要定位弹窗元素,稳定性较差,作为保底方案。实操心得:对于内部测试包,我强烈推荐方法2(ADB预授权),干净彻底。对于无法预知权限场景的情况,方法1(
autoGrantPermissions)是首选。务必在真机上测试权限处理逻辑,因为模拟器的权限行为可能与真机有差异。
3.2 拨号操作的元素定位与交互
拨号界面通常包含数字键盘、拨号按钮、联系人输入框等。定位这些元素是第一步。
使用Appium Inspector定位:打开拨号盘,用Inspector查看。理想的定位依据是
resource-id(安卓)或accessibility id(iOS/安卓),因为它们是唯一且稳定的。例如,数字键“1”的id可能是com.android.dialer:id/one。编写稳健的拨号脚本:假设我们测试拨打号码“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_elements与find_element:find_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 设计多维度的断言条件
一个健壮的通话自动化测试用例,其断言不应该只有“接通了”这么简单。我们需要从多个维度验证:
状态断言:如上所述,核心断言是通话状态必须从
DIALING(拨号中)转变为ACTIVE(通话中)。这是最基本的要求。时长断言:测试通话保持功能。脚本可以在通话建立后,等待一个预设的时长(例如30秒),然后再次检查通话状态是否仍为
ACTIVE。如果中途断线,测试失败。UI辅助断言:虽然不作为主要依据,但可以辅助验证。例如,检查通话界面是否显示了正确的联系人姓名或号码(通过定位对应的TextView)。这可以验证呼叫的目标是否正确。
音频通道断言(进阶):通过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 多种挂断方式及其适用场景
挂断电话,至少有三种可靠的自动化实现方式,各有优劣:
通过UI元素点击挂断按钮:这是最接近用户操作的方式。
# 定位通话界面的大红色挂断按钮 end_call_button = driver.find_element(AppiumBy.ID, 'com.android.dialer:id/incall_end_call') end_call_button.click() print("通过UI点击挂断。")优点:完全模拟用户。缺点:按钮的ID可能因ROM不同而变化,稳定性最差。
发送ADB输入事件模拟按下“挂断键”:安卓系统有一个虚拟的“结束通话”键值。
import subprocess subprocess.run('adb shell input keyevent KEYCODE_ENDCALL', shell=True) print("通过ADB模拟挂断键挂断。")优点:通用性强,几乎在所有安卓设备上都有效。缺点:这是一个全局事件,如果当时有其他应用在前台并响应了这个键值,可能会产生意外效果。
使用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 通话记录清理与测试隔离
自动化测试,尤其是会往服务器或本地数据库写入数据的测试,必须考虑数据清理,防止测试数据堆积影响后续测试或产生脏数据。
清理本地通话记录:对于手机自带的拨号应用,通话记录会保存在本地。我们可以通过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使用隔离的测试账号/号码:如果测试的是像微信语音这样的网络通话,强烈建议使用专门为此自动化测试创建的、隔离的测试账号。这些账号只用于自动化测试,不与真实业务数据混淆。脚本应在
setUp中登录测试账号A,在另一台设备或模拟器上登录测试账号B。脚本层面的清理:在测试用例的
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间接判断音频系统是否在工作。
检查音频进程:通话建立后,系统会有相关的音频处理进程(如
mediaserver,audioserver)活跃,或者电话应用本身会持有音频焦点。adb shell ps | grep -E ‘(mediaserver|audioserver|audio)’通过对比通话前后这些进程的状态或资源占用,可以侧面验证音频通道是否被激活。
监控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)或使用云测平台的服务。
一种可行的思路:
- 在两端(主叫和被叫)的测试环境中,预装一个自定义的“音频分析助手”App。
- 通话建立后,自动化脚本通过ADB或广播,通知这个助手App开始播放一段标准的测试音频(例如一段特定的正弦波或语音)。
- 另一端的助手App同时开始录音。
- 通话结束后,助手App分析录音文件,通过算法(如互相关算法)计算出音频从发送到接收的延迟,并分析音频失真程度。
- 自动化脚本再从助手App拉取分析结果,作为测试断言的一部分。
这实现起来比较复杂,属于专项测试的范畴。但对于核心的语音通话产品,这种投入是值得的。
6.3 使用网络模拟工具测试弱网场景
语音通话质量与网络状况强相关。自动化测试必须覆盖弱网、高延迟、高丢包等场景。
工具选择:
- Charles/ Fiddler:适合模拟手机HTTP/HTTPS代理下的网络状况,但对纯TCP/UDP的语音流(如WebRTC)可能不直接生效。
- Android Emulator 自带网络模拟:Android Studio的模拟器可以直接设置网络速度和延迟,非常方便。
- 硬件设备+网络损伤仪:最真实但成本高,用于实验室环境。
- 软件方案:
netem(Linux) 或Network Link Conditioner(macOS):如果你使用电脑开热点给手机,可以在电脑端用这些工具模拟网络损伤。
在自动化脚本中集成网络模拟:以使用
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。 - 排查:
- 确认界面:首先通过
driver.get_screenshot_as_file('debug.png')截图,或者用driver.page_source打印当前页面XML源码,确认是否真的跳转到了你期望的界面。 - 检查定位符:用Appium Inspector重新连接设备,查看目标元素的属性是否变了。特别是
resource-id,有些应用在不同版本或不同状态下会动态生成。 - 等待时机:元素还没加载出来你就去点击了。永远不要只用
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() - 备用定位策略:如果ID不稳定,尝试用其他属性组合定位,比如
XPath(但尽量少用,性能差且易变)、accessibility id(对于有内容的元素)、class name配合text等。 - 处理动态ID:如果ID是动态的(包含时间戳或随机数),尝试用
XPath的contains、starts-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。主叫拨号后,可以轮询查询自身状态是否为DIALING或OFFHOOK,确认拨号请求已发出。被叫端则轮询检查来电状态(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会话,而不是每个用例都重新启动应用。在setUpClass和tearDownClass中管理驱动生命周期。
把这些技巧融入到你的脚本和测试框架中,能极大提升自动化测试的稳定性和执行效率,让它从“偶尔能跑通”变成“持续稳定运行”的可靠质量守护环节。