1. 为什么我开始认真写Appium这套东西
提到Appium,干过移动端测试的朋友应该都不陌生。我最早接触它的时候,还是在Android 4.4横行的年代,那时候自动化测试圈子里的主流选择是Robotium和MonkeyRunner,Appium还只是个小众框架。但我当时在项目里踩了太多Native控件和WebView控件混在一起没法统一处理的坑,于是认真调研了一圈,最后决定全面转向Appium。
先说结论:Appium真正吸引我的地方,不是它跑得多快、脚本写得多炫,而是它把“跨平台”和“多语言”这两件最折磨测试开发的事给做了统一。同一套业务逻辑,Android和iOS可以共享绝大部分代码,而且你不需要为了写脚本去学一门新语言——Java、Python、Ruby、JavaScript都可以直接上手。这对于团队里既有Java后端背景、又有Python脚本功底的测试同学来说,简直是救命稻草。
这个教程不会只停留在“怎么装环境、怎么点按钮”的层面,我更想把这几年来实际项目中踩过的坑、验证过的方案、以及一套真正能跑起来的落地流程,一并整理出来。无论你是刚转行做测试的小白,还是已经在用其他框架想迁移过来的老兵,这篇文章的实操步骤和排查思路应该都能给你节省不少时间。
2. 开始之前:Appium到底解决了什么问题
2.1 自动化测试在移动端的核心矛盾
先聊一个比较基础但很关键的问题:为什么移动端自动化测试这么难做?我见过很多团队,前一个月还好好的,后一个月维护成本直接压垮了整个测试组。核心矛盾其实就三个字:碎片化。
这里说的碎片化不只是手机品牌多、屏幕尺寸杂,更重要的是技术栈的碎片化。同一个App里,Android端有原生页面、有H5页面、甚至还有Flutter或者React Native渲染的页面。如果用一个测试框架只能处理原生控件,那当你走到WebView页面的时候,脚本就会当场卡死,所有用例全部红掉,然后测试组的同学就开始通宵手工回归。
Appium的设计初衷,很大程度就是来解决这个问题的。它基于WebDriver协议,额外扩展了一套Mobile JSON Wire Protocol,让你可以用同一套API去操作原生控件、WebView控件,甚至是混合应用里的元素。你在PC浏览器上写Selenium的那套思路,比如通过XPath找元素、通过id定位按钮、通过sendKeys输入文本,到了Appium这套体系里几乎可以无缝迁移过来。
2.2 为什么是Appium而不是其他框架
说实话,现在市面上能选的框架并不少。UiAutomator2是Google官方的方案,性能和稳定性其实很好,但它绑定了Java语言,iOS又用不了;XCUITest是苹果家的东西,只服务iOS;Espresso在Android上很快,但也只服务Android。如果你只想写单端的脚本,这些原生框架完全够用,但一旦想让一套团队技能同时覆盖双端,它们就变成一个很尴尬的存在。
Appium则把“Client代码”和“服务端执行”彻底解耦。你在测试机上运行的是Appium Server,它通过监听4723端口接收来自客户端的命令,然后针对不同平台把这些命令转换成对应的自动化指令。这意味着前端无论用什么语言去写,最终都是走HTTP请求到服务端,服务端再去驱动设备干活。这个架构虽然多了一层网络开销,带来了大概几十毫秒的额外延迟,但在当前这种硬件性能和设备响应速度条件下,这点损耗完全可以接受,换来的是统一的编程模型和跨平台能力。
2.3 一个核心概念先搞明白:Session
Appium整个工作流程,如果只记一个词,那就是Session。你可以把它理解成“一场测试活动的上下文”。客户端发起Desired Capabilities配置,服务端接收这些配置后,根据配置去拉起指定App,然后返回一个Session ID。之后你做的所有查找元素、点击、滑动、输入操作,都必须带上这个Session ID,服务端才能知道这些命令要作用在哪台设备、哪个App上。
我见过很多新手在配置阶段就卡住了,Caps写了一大堆,但是没弄明白每个参数的意义。其实核心的也就那么几个:
- platformName:告诉服务端你要测Android还是iOS。
- deviceName:虽然对Android没有强制约束,但最好填adb devices看到的序列号,方便定位设备。
- appPackage / appActivity:指定要启动的App及入口Activity,这是Android平台最关键的参数。
- noReset:如果设为true,Appium不会在每次测试前清空应用数据。这个参数在调试阶段特别重要,否则你每次都要重新走一遍引导页和登录流程,能把人折磨疯。
等你理解了Session,后面所有的脚本编写其实都是在和这个Session打交道。所以不要一上来就背API,先把会话机制搞清楚,后面就顺了。
3. 完整环境搭建:每一步都给你踩坑避雷
3.1 需要的软件清单
Appium这玩意,它不是一个单独的安装包装完就万事大吉的。整个链路涉及的东西比较多,我列一份我目前Windows笔记本上实际在用的组合,你照着装就行,版本不一定完全一致,但建议接近:
| 软件 | 版本建议 | 用途说明 |
|---|---|---|
| Java JDK | 1.8 或 11 | Android SDK编译和Appium Server运行依赖 |
| Android SDK | API 30 及以上 | adb工具、构建工具、平台工具 |
| Node.js | 12 或 14 LTS | Appium Server的运行时 |
| Appium Desktop | 2.x | 自带Server和Inspector可视化工具 |
| Appium Inspector | 最新版 | 元素定位辅助工具 |
| Python | 3.6+ | 我的脚本语言,用于编写测试用例 |
| Appium-Python-Client | 2.x | Python客户端的SDK |
| 夜神 / MuMu / 官方模拟器 | 任意 | Android模拟器,建议API 28以上 |
| Appium-Python-Client | 2.x | 客户端SDK |
3.2 环境变量的坑,十个人里八个栽在这
安装完JDK和Android SDK之后,接下来就是配置环境变量。这也是我见过新手最容易卡壳的地方,而且报错信息往往很模糊,比如在cmd里输入adb,直接提示“不是内部或外部命令”。这个问题的原因只有一个:环境变量没配好,cmd找不到adb.exe的路径。
Android SDK的环境变量需要配置这么几项:
ANDROID_HOME:你的SDK安装路径(比如 D:\Android\Sdk) PATH新增:%ANDROID_HOME%\platform-tools PATH新增:%ANDROID_HOME%\tools PATH新增:%ANDROID_HOME%\build-tools\30.0.3Java部分,配置JAVA_HOME指向JDK安装目录,然后在PATH里加入%JAVA_HOME%\bin。配置好之后,别急着往下走,先打开一个全新的cmd窗口,分别输入以下命令验证:
java -version adb --version node -v只要这三条命令都能正常输出版本号,说明基础环境已经通了。如果哪一步没有输出,就回头检查对应的环境变量路径,这一步偷懒,后面跑脚本的时候会报出一堆无法理解的错误。
3.3 安装Appium Server的两种方式
Appium Server的安装路径有两条:一是在Appium Desktop的界面里直接启动Server,二是通过npm命令行安装。我个人的建议是:调试阶段用Appium Desktop,因为它自带一个可视化界面,能直接看到Server的日志输出和监听状态;但如果你打算写脚本做持续集成,那就必须用npm安装的Appium,因为命令行方式可以被脚本直接拉起,CI环境里不需要GUI。
npm install -g appium appium --version安装完之后,你在cmd里敲appium,看到listener started on 0.0.0.0:4723这行日志,说明Server已经成功启动了。这里的4723就是Appium Server的默认端口,所有客户端请求都会发到这个端口上。
顺便提一句,如果npm安装速度很慢,可以考虑把registry切换到国内源:
npm config set registry https://registry.npmmirror.com装完之后如果还嫌慢,那大概率是网络链路问题,等一等就行,不要反复去卸载重装,反而容易搞出环境残留问题。
3.4 Android模拟器与真机怎么选
模拟器这边,Google官方的AVD速度和稳定性都比较均衡,但我个人日常更常用的是夜神或MuMu这类第三方模拟器,原因只有一点:在国内复杂网络环境下,官方模拟器拉取镜像经常失败,第三方模拟器直接下载安装包就能用,省心不少。
不过第三方模拟器有一个坑:默认情况下,adb可能识别不到设备。这是因为模拟器监听的端口不是默认的5555。解决方法是手动连接模拟器端口:
adb connect 127.0.0.1:62001不同模拟器对应的端口不一样,夜神是62001,MuMu是7555,具体可以在模拟器设置里查看。连接成功后,输入adb devices应该能看到127.0.0.1:62001 device这样的状态。这一步如果没做,Appium Server启动后会报“No devices found”错误,测试根本跑不起来。
真机调试的话,需要打开开发者选项里的“USB调试”,并且安装对应厂商的USB驱动。现在手机厂商普遍做了随机MAC地址的机制,导致每次插拔手机adb设备号会变化,建议在代码里通过adb devices动态获取设备号,而不是写死。
4. Desired Capabilities 配置:脚本能跑起来的前提
4.1 必须弄懂的核心参数
Desired Capabilities可以理解成“启动会话的钥匙串”。你告诉Appium Server需要启动什么平台、什么App、以什么模式执行,服务端才能正确拉起环境。配置不对,后面所有步骤全部白搭,而且报错信息经常是让人一头雾水的。
这是我在Android模拟器上跑通的一套基础配置:
desired_caps = { "platformName": "Android", "deviceName": "127.0.0.1:62001", "appPackage": "com.example.app", "appActivity": ".MainActivity", "noReset": True, "unicodeKeyboard": True, "resetKeyboard": True }platformName和deviceName前面说过了,重点说一下appPackage和appActivity怎么拿。很多人一开始都会在这里卡住,不知道自己的应用这两个值应该填什么。这里有一个非常实用的命令:
# 先将App在模拟器或真机上启动 adb shell dumpsys window | grep mCurrentFocus执行之后,输出的结果里会显示类似mCurrentFocus=Window{... com.example.app/.MainActivity}的内容,斜杠前面就是appPackage,后面就是appActivity。这个方法是190%可靠的,比在源码里翻AndroidManifest.xml快得多。
4.2 noReset 和 unicodeKeyboard 是调试阶段的保命符
如果你是在开发阶段调试脚本,noReset一定要设为True。不然每跑一次用例,Appium都会清除App的数据,你的登录状态、引导页开关、推荐算法缓存全部被清空,每次都要从头走一遍流程,这感觉谁用谁知道。
unicodeKeyboard和resetKeyboard这两个参数,主要是为了解决中文输入问题。Android原生环境的输入法有时候接不住从Appium发送的中文字符,需要先调用系统自带的Unicode输入法接管,然后等输入完成之后再恢复原状。这两个参数可以一并设为True,基本可以规避掉90%以上的中文输入坑。
4.3 报错了两行泪:常见配置错误速查
配置阶段最容易遇到的报错大致有这几类:
- “Parameters were incorrect”:通常是desired_caps里写了不支持的参数名或者参数值格式不对,仔细对照官方文档检查拼写。
- “No such file or directory”:如果你指定了app字段指向本地apk,但是这个路径不存在,Appium Server会直接抛错。建议在项目里建一个apps目录统一存放测试包。
- “Original error: Could not find a connected Android device.”:设备没有正确连接。用
adb devices看看设备状态是不是device,如果是unauthorized,需要在手机上点一下“允许USB调试”弹窗。
配置这个环节,我个人的建议是:不要图省事把所有参数一次性写完再启动,而是先写一个最小化配置,确认Session能起来、App能拉到前台,再逐步增加参数。这样出了问题,定位的范围会小很多。
5. 用Python写第一个自动化脚本
5.1 安装客户端库
接下来的所有示例,我都用Python来写,因为它在测试行业的普及度实在太高,而且语法简洁,适合快速迭代。先安装Appium的Python客户端库:
pip install Appium-Python-Client注意:这里装的是Python Client库,不是Appium Server本身。Server在上面已经装过了,这两个东西是独立存在的,新手经常会搞混。
5.2 第一个脚本:打开App并点击一个按钮
装好之后,写一个最基础的脚本:
from appium import webdriver import time desired_caps = { "platformName": "Android", "deviceName": "127.0.0.1:62001", "appPackage": "com.example.app", "appActivity": ".MainActivity", "noReset": True } # 连接Appium Server,启动会话 driver = webdriver.Remote("http://127.0.0.1:4723/wd/hub", desired_caps) time.sleep(3) # 通过resource-id定位元素并点击 element = driver.find_element_by_id("com.example.app:id/btn_start") element.click() time.sleep(2) # 验证页面是否跳转成功 assert "成功跳转" in driver.page_source # 关闭会话 driver.quit()这段代码的逻辑很简单,但它示范了Appium测试脚本的完整生命周期:建立连接、启动会话、查找元素、执行操作、验证结果、关闭会话。你可以在这个骨架上不断扩展,加上断言、日志、异常处理,慢慢就会形成一个完整的测试框架。
5.3 定位元素的几种姿势,推荐优先级一次说清楚
自动化测试里,元素定位是日常写脚本时最频繁的操作,也是最容易脆弱的环节。前端稍微改个id,或者布局重构一下,定位器可能就失效了。我根据自己的实战经验,把定位方式的优先级从高到低排个序:
- resource-id定位:Android原生控件最稳定可靠,形如
com.example.app:id/btn_start,首选。 - accessibility id(content-desc):可访问性属性,在Android上对应contentDescription,适合有做无障碍适配的应用。
- XPath文本定位:比如
//android.widget.TextView[@text='登录'],优点是灵活,但性能差、维护成本高,不建议在循环里大量使用。 - class name定位:按控件类型定位,比如
android.widget.EditText,但同类型控件可能有很多个,通常需要配合下标使用,能用但容易乱。 - UIAutomator定位:Android平台的进阶玩法,可以直接写UI Automator语法,比如
new UiSelector().text("确认"),比较强大,但调试起来相对复杂。
我在实际项目里的经验是:优先保证元素有稳定的id或content-desc,写脚本的时候就会非常清爽。如果你的App还没做可测试性改造,也没关系,可以要求开发帮忙给控件补上资源id,这本身就是测试驱动开发的一部分。
5.4 一个业务流程的完整脚本示例
单点击一个按钮没什么感觉,我拿一个真实的登录场景来演示。假设App的登录页有两个输入框和一个登录按钮,输入账密后点击登录,预期跳转到首页:
from appium import webdriver from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from appium.webdriver.common.mobileby import MobileBy import time desired_caps = { "platformName": "Android", "deviceName": "127.0.0.1:62001", "appPackage": "com.example.app", "appActivity": ".LoginActivity", "noReset": True, "unicodeKeyboard": True, "resetKeyboard": True } driver = webdriver.Remote("http://127.0.0.1:4723/wd/hub", desired_caps) driver.implicitly_wait(10) # 输入账号 account_input = driver.find_element(MobileBy.ID, "com.example.app:id/et_account") account_input.clear() account_input.send_keys("test_user_001") # 输入密码 pwd_input = driver.find_element(MobileBy.ID, "com.example.app:id/et_password") pwd_input.clear() pwd_input.send_keys("Abc@123456") # 点击登录 login_btn = driver.find_element(MobileBy.ID, "com.example.app:id/btn_login") login_btn.click() # 显式等待,等待首页元素出现 try: home_element = WebDriverWait(driver, 10).until( EC.presence_of_element_located((MobileBy.ID, "com.example.app:id/tv_home_title")) ) print("登录成功,已进入首页") except Exception as e: print("登录失败,页面未跳转") print(driver.page_source) finally: driver.quit()这里有几个值得一说的小细节。第一,implicitly_wait(10)设了全局隐式等待,所有元素查找都会等待最多10秒,这样可以在元素加载稍微慢一点的情况下,尽量避免NoSuchElementException。第二,登录按钮点击后,页面跳转是需要时间的,用WebDriverWait显式等待首页元素出现,比硬编码time.sleep(5)要健壮得多。我在写脚本的时候,显式等待优先级永远高于time.sleep,这是一个能让脚本稳定性提升一个档次的好习惯。
5.5 滑动、点击坐标等特殊操作
除了常规的点击和输入,移动端测试里还有两个高频操作是Web自动化里不太碰到但Appium里非常常用的:滑动和坐标点击。
# 从点(x1,y1)滑到点(x2,y2),整个过程耗时300毫秒 driver.swipe(x1, y1, x2, y2, duration=300) # 通过坐标直接点击,手机分辨率不同,坐标也会变化,所以只建议临时用 driver.tap([(x, y)], duration=100)需要特别提醒的是,坐标点击是一个“能不用就不用”的操作。只要元素能通过id定位到,就用id,因为坐标会随着设备分辨率变化而失效。但如果你的需求是滑动轮播图、左滑删除、下拉刷新这类没有现成控件属性的操作,用坐标就是最直接的方式。
还有一个获取屏幕尺寸的小技巧,可以用来按比例计算滑动起点和终点:
size = driver.get_window_size() start_x = size["width"] // 2 start_y = int(size["height"] * 0.8) end_x = size["width"] // 2 end_y = int(size["height"] * 0.2) driver.swipe(start_x, start_y, end_x, end_y, duration=500)这样写,脚本在不同分辨率设备上都能正常滑动,不用为每台设备单独调参。
6. 用好Appium Inspector:元素定位的效率工具
6.1 为什么要用可视化工具
很多新手刚开始写Appium脚本的时候有个误区:对着App猜元素属性,感觉这个位置像是个Button,然后就盲写代码,跑不起来再慢慢试。这种方式效率太低了。正确的姿势应该是:先用Appium Inspector打开你的App,看一眼控件树,确定你要操作的元素长什么样、该用什么属性去定位,然后再写脚本。
Appium Inspector在Appium 2.x里默认是包含的,直接在Appium Desktop里点击放大镜图标就能打开。它连接上Session之后,左边是屏幕截图,右边是控件层级树,点击某个控件,还能看到这个控件的所有属性,包括resource-id、class、text、bounds等。有了这些属性,写定位器的准确率几乎是100%。
6.2 用Inspector快速拿到定位信息
打开Inspector的基本步骤是:
- 启动Appium Server。
- 在Appium Desktop主页点击Inspector按钮。
- 输入和脚本一致的Desired Capabilities配置。
- 点击Start Session,Inspector会自动拉起你的App并生成当前页面的控件树。
- 点击你想定位的元素,右侧面板会显示完整属性。
这个流程唯一要注意的是,Inspector启动的Session和你脚本后续启动的Session是两回事,不要同时操作,否则两个会话会互相干扰,导致App被来回重启。我建议先在Inspector里完成元素侦察,然后退出Inspector,再运行你的测试脚本。
6.3 Inspector常见的连接问题
我见过太多人在Inspector这一步翻车了,主要症状有:
- Session创建超时:大概率是desired_caps填错,尤其注意appPackage和appActivity的大小写,大小写错误几乎无法直接看出问题,但就是连不上。
- 屏幕截图一直是黑屏:模拟器的GPU渲染和Appium的截图机制冲突,可以尝试在模拟器设置里把GPU模式改成软件渲染。
- 控件树为空:确认你选的platformName和实际设备一致,有时候Inspector会默认连上次的Session配置,导致连到了不存在的设备上。
Inspector用熟练之后,你会发现写元素定位这事基本就是“抄作业”,照着控件属性写就行,根本没多少技术含量。
7. 把脚本变成一个能跑起来的测试框架
7.1 从脚本到框架:为什么要做封装
如果只是测试几个核心场景,那写脚本就够了。但一旦用例数量增长到几十条上百条,直接裸写脚本就会面临三个问题:大量重复的初始化代码、元素定位信息散落在各个用例里、用例失败时连日志都捞不到。这时候就需要做框架层面的封装。
我比较推荐的分层结构是:把设备连接、启动App、关闭会话这些公共逻辑抽到BaseDriver里;把页面操作封装成Page Object类;把测试用例写在专门的test目录下;断言和报告用pytest来组织。
# base_driver.py from appium import webdriver class BaseDriver: def __init__(self, desired_caps): self.driver = webdriver.Remote("http://127.0.0.1:4723/wd/hub", desired_caps) def find(self, locator): return self.driver.find_element(*locator) def click(self, locator): self.find(locator).click() def input_text(self, locator, text): element = self.find(locator) element.clear() element.send_keys(text) def swipe_up(self): size = self.driver.get_window_size() start_x = size["width"] // 2 start_y = int(size["height"] * 0.8) end_y = int(size["height"] * 0.2) self.driver.swipe(start_x, start_y, start_x, end_y, duration=500) def quit(self): self.driver.quit()这样封装之后,写用例的地方基本就是在描述业务操作本身,代码可读性和维护性都提升了很多。
7.2 Page Object模式:塞进项目里很香
Page Object模式几乎是测试框架的标准姿势了。核心思想其实很简单:一个页面对应一个类,页面上所有操作逻辑都放在这个类里,测试用例只负责编排动作和验证结果,不直接接触元素定位细节。
# pages/login_page.py from appium.webdriver.common.mobileby import MobileBy class LoginPage: ACCOUNT_INPUT = (MobileBy.ID, "com.example.app:id/et_account") PWD_INPUT = (MobileBy.ID, "com.example.app:id/et_password") LOGIN_BTN = (MobileBy.ID, "com.example.app:id/btn_login") def __init__(self, driver): self.driver = driver def input_account(self, account): element = self.driver.find_element(*self.ACCOUNT_INPUT) element.clear() element.send_keys(account) def input_pwd(self, pwd): element = self.driver.find_element(*self.PWD_INPUT) element.clear() element.send_keys(pwd) def click_login(self): self.driver.find_element(*self.LOGIN_BTN).click()这样做的最大好处是:当登录页的UI从“账号密码框+登录按钮”改成“验证码登录”或者“一键登录”,我只需要更新LoginPage这一个类,所有关联的测试用例都不需要改动。如果你的App迭代频繁,UI动不动就变,Page Object模式能帮你省出大量的维护时间。
7.3 pytest + 数据驱动:让用例量跑起来
框架最后一个拼图是测试执行层。我推荐用pytest,它天生支持fixture、参数化和丰富的断言,并且生态里有allure-pytest可以直接生成漂亮的可视化测试报告。
test_cases/ ├── test_login.py ├── test_order.py └── conftest.py在conftest.py里定义一个设备初始化的fixture:
import pytest from appium import webdriver @pytest.fixture(scope="module") def driver(): desired_caps = { "platformName": "Android", "deviceName": "127.0.0.1:62001", "appPackage": "com.example.app", "appActivity": ".LoginActivity", "noReset": True } driver = webdriver.Remote("http://127.0.0.1:4723/wd/hub", desired_caps) yield driver driver.quit()然后用参数化来跑多组数据:
import pytest from pages.login_page import LoginPage class TestLogin: @pytest.mark.parametrize("account,password", [ ("user01", "Pass@123"), ("user02", "Pass@456") ]) def test_login(self, driver, account, password): login_page = LoginPage(driver) login_page.input_account(account) login_page.input_pwd(password) login_page.click_login() assert "登录成功" in driver.page_source跑上面的用例,只需在项目根目录执行:
pytest -v --alluredir ./report这一套流程下来,你的自动化测试已经从“脚本”升级为“框架”了。之前那种一条用例写一个月、上线两个月全废气的情况,会得到很明显的改善。
8. 常见问题与排查技巧实录
8.1 高频报错排查表
做Appium的时间长了,会发现大部分报错都集中在几个固定场景里。我整理了一份排查速查表,基本覆盖了我日常工作中80%以上的问题:
| 报错信息 | 可能原因 | 处理方式 |
|---|---|---|
| Error: Could not find a connected Android device | 设备未连接或adb未识别 | adb devices检查设备状态,确认设备正常;模拟器则先adb connect 127.0.0.1:端口 |
| Element not found | 元素定位器失效,或页面尚未加载完成 | 优先用Appium Inspector查看最新的控件id,配合显式等待 |
| SessionNotCreatedException | desired_caps配置有误 | 检查appPackage、appActivity是否正确,platformName拼写是否正确 |
| Unknown command: wd/hub | Appium版本和客户端版本不匹配 | Appium 2.x与较老客户端库存在兼容问题,升级Appium-Python-Client到2.x |
| TimeoutException | App启动太慢或页面加载太久 | 提高WebDriverWait的超时时间或者检查noReset配置 |
| adb server version doesn't match this client | 多个adb版本冲突 | 在环境变量里统一指定一个SDK版本,或结束adb进程后重新启动 |
8.2 模拟器和真机之间切换时容易踩的坑
不少团队是开发阶段用模拟器,回归阶段用真机,这就会遇到几个典型的坑。模拟器上跑得好好的用例,换到真机上,第一反应就是满屏失败。最常见的原因有两个:
第一,分辨率不同。你在模拟器上滑动距离是写死的,真机屏幕更大,指定距离可能就够不到目标元素。所以在写滑动操作的时候,尽量基于get_window_size()做比例计算,而不是写死像素数。
第二,权限弹窗。模拟器很多系统弹窗是默认关掉的,真机上跑用户协议弹窗、位置权限弹窗、通知权限弹窗一个接一个地冒出来。遇到这种情况,要么在用例里写一个处理弹窗的通用函数,比如判断当前页面有没有“允许”按钮,有就点掉,要么在设备初始化阶段统一预授权。
8.3 测试不稳定?先从这五个方向排查
测试不稳定,业界叫“flaky tests”,这几乎是所有自动化测试团队绕不开的痛点。同一个用例,这次跑通过了,下次就跑挂了,而且挂的位置还不一样。我个人的排查顺序是这样:
- 先看元素加载是否足够快。把隐式等待设为10秒,或者把关键的页面跳转改成显式等待。
- 再看元素是否唯一。同一个页面上如果存在多个相同id或相同文本的控件,find_element只会返回第一个,这时候如果UI顺序变化,脚本就会点错。
- 然后看输入框有没有清空。不清空输入框直接send_keys,后面的内容会接在已有内容后面,实测这种问题很隐蔽。
- 接着看App的前后状态。如果上一个用例没有正常退出,残留页面会直接导致下一个用例定位不到元素。
- 最后检查设备状态。跑太久的内存占用、过热降频、后台网络切换,都会导致测试中断。
8.4 一个小技巧:日志和截图能救你于水火
用例失败不可怕,可怕的是失败了你不知道它为什么挂。我强烈建议在用例的finally块里强制截屏:
def test_something(driver): try: # 测试步骤 pass except Exception: # 失败时截屏 driver.save_screenshot("./screenshots/test_something_failed.png") raise finally: # 无论如何都要把当前页面源码保存下来 with open("./logs/test_something_page_source.xml", "w", encoding="utf-8") as f: f.write(driver.page_source)页面源码XML文件会记录当前页面的完整控件树,这个文件的价值在于:即使截图黑屏或者被系统弹窗挡住了,你依然可以从XML里看到页面到底发生了什么。
9. 我把这个教程落到实际项目的体会
写到这里,最后聊点实在的。我从决定用Appium到把一个项目的关键链路全部自动化,前前后后经历了大概两个月。第一个月的感受是“这也能报错?”,第二个月开始才逐渐摸清它的脾气,等到第三个月,脚本已经能稳定跑完所有核心回归测试,我开始越来越觉得这套工具值得投入。
如果让我给刚入坑的你一条建议,那就是:不要急着用框架去炫技,先把最基本的“启动App、点击、输入、断言、退出”这几个动作跑得滚瓜烂熟。然后接Page Object模式,再接数据驱动,再接持续集成,一步一步来。
另一个很深刻的体会是:Appium本身并不难,真正难的是你对自己产品业务逻辑的理解。自动化测试的维护成本,本质上等于业务变化成本和测试代码耦合度之间的乘积。你把业务理解得越透彻,写出来的测试结构就越贴近真实场景,维护的时候就越不容易被产品迭代打乱节奏。
最后分享一个小扩展方向:现在的Appium已经可以通过driver.execute_script("mobile: shell", ...)直接执行adb shell命令,这很有用。比如你可以在测试里用shell命令去设置系统时区、修改网络状态、模拟弱网,这些高级用法,能让你离“完美还原线上场景”更近一步。等基础功能玩透了,尝试往这个方向深入,应该会很有意思。