你有没有过这种经历:一个App要回归测试,几十条用例,手动点来点去,点得手抽筋,还总是漏测。我自己的解决思路很简单,把重复操作交给Python,让它在安卓设备上自己跑。从选Appium到搭框架,前后也踩了一堆坑,这篇文章就是一次完整复盘。如果你正打算做安卓自动化测试,或者已经在用Python但环境老是搞不定,这篇文章可以帮你省下大把时间。这里提到的方案,我实际是在HoRain云上跑的,思路和本地完全通用。
1. 项目概述与整体思路
1.1 为什么选择Python + Appium组合
技术选型的时候,我第一反应不是Appium。当时也考虑过UIAutomator、Robotium、Espresso这些,但最后都放弃了。原因是这几个框架跟安卓的绑定太紧,脚本基本只能写Java或者Kotlin,团队成员不一定都熟。而Appium走的是WebDriver协议,底层把uiautomator2、XCUITest这些都封装好了,上层可以用任意语言写脚本。我选了Python,因为Python语法简单,写测试脚本的速度快,而且pytest的断言、fixture、参数化都很顺手,和Appium搭配起来几乎是社区里最多的组合。
还有一个现实问题:移动端碎片化严重,不同厂商的ROM、不同安卓版本的控件树差异很大。Appium的社区插件多,遇到问题能找到很多现成的解决方案,比另起炉灶要稳。所以从学习成本和长期维护的角度看,Python + Appium不是“最好的”,但一定是“最容易活下来”的选择。
1.2 自动化测试的整体流程
自动化不是上来就写脚本。我一般会先梳理一遍业务用例,把高频、稳定、重复量大的场景挑出来,比如登录、注册、列表滑动、下单流程,然后才开始设计脚本。
整个执行链路大概是这样的:测试脚本通过Appium-Python-Client发起请求,Appium Server收到请求后,再把它转发给手机上安装的uiautomator2服务,uiautomator2负责真正操作界面元素。如果是云端设备,比如我在HoRain云上创建的安卓真机,Appium Server同样可以连接到那台远程设备上,脚本不用管设备在哪里,对业务代码完全透明。
这样的好处是,脚本只关注业务逻辑,设备层的事全部交给Appium去处理。加上pytest管理用例,可以轻易做到数据驱动、失败重跑、生成报告,整个体系是以后端接口测试那套思路平移过来的,跑起来非常顺。
2. 环境搭建全攻略
2.1 基础环境准备:JDK与Android SDK
先把本地基础环境捋一遍。Appium在安卓上依赖uiautomator2,而uiautomator2是Java写的,所以JDK必须装。我建议装JDK 8或11,Appium官方对这两个版本兼容最好。装上之后要配JAVA_HOME环境变量,不然Appium起不来。
接着是Android SDK。如果你安装了Android Studio,可以直接用它的SDK,但它比较重。我个人的做法是单独下载command-line tools,只装platform-tools和build-tools两个组件,这样最干净。装好以后需要配ANDROID_HOME,并把platform-tools目录加进PATH,这样adb命令才能全局用。装完以后,终端执行adb version,看到版本号就说明基础环境没问题。
模拟器我试过好几个,MuMu、雷电、蓝叠都用过,稳定性其实差别不大,关键是选你真机同Android版本的模拟器。也可以用Android Studio自带AVD,虽然启动慢点,但跟SDK的结合最紧,调试Appium时我一般优先用它。
2.2 Python环境与Appium工具链安装
Python我建议用3.9以上版本,直接去官网下安装包,安装时记得勾选“Add Python to PATH”。装完之后先建一个虚拟环境,不要一股脑把依赖装到全局,后面项目多了会打架。用python -m venv venv创建,然后激活。
接下来是Appium Server。早期版本还需要单独装Appium Desktop,现在已经整合到Appium Inspector里了。我这里用的是Appium 2.x,它把驱动拆成插件机制,装起来更灵活。安装命令很简单:npm install -g appium。装完以后,还要安装安卓驱动:appium driver install uiautomator2。
Python客户端也要装,执行pip install Appium-Python-Client。这是Appium官方提供的客户端库,脚本里通过它连接Appium Server。装完后可以跑一下appium-doctor检查环境,该工具会告诉你哪些配置有问题,省得自己排查。
2.3 设备连接与模拟器联网细节
设备连接是很多新手第一个卡住的地方。真机要打开开发者选项,在系统设置里连续点击版本号,然后开启USB调试。插上数据线后,用adb devices查看设备状态,如果显示device而不是unauthorized,就说明连接成功。模拟器稍微不一样,每个模拟器会监听一个本地端口,比如MuMu的7555,雷电的5555。如果adb找不到模拟器,可以用adb connect 127.0.0.1:端口手动连。
“安卓虚拟机怎么联网”这个问题也经常会遇到。模拟器默认走NAT模式,能上外网,但有时候Appium脚本里要访问本地服务,比如测试App的后端接口写的是http://10.0.2.2:8080,模拟器里访问宿主机必须用这个地址,不能用127.0.0.1。如果是云端设备,像我在HoRain云里面开的安卓实例,平台一般会提供一个连接地址,直接用adb connect就行,不用管什么端口映射。
这里有个容易踩的坑:模拟器启动后,Launcher首页会导致Appium找不到目标控件,所以最好在capabilities里设置noReset为True,减少系统弹窗的干扰。
3. 核心实操:从零编写第一个自动化脚本
3.1 Desired Capabilities配置详解
写脚本的第一步是告诉Appium要操作哪个设备。这个信息放在一个字典里,叫Desired Capabilities。我最常用的几个参数如下表所示:
| 参数名 | 含义 | 示例值 |
|---|---|---|
| platformName | 平台系统 | Android |
| deviceName | 设备标识 | 127.0.0.1:7555 |
| appPackage | 应用包名 | com.example.app |
| appActivity | 启动Activity | .MainActivity |
| noReset | 是否重置应用数据 | True |
| automationName | 驱动类型 | UiAutomator2 |
先给个最简示例:
from appium import webdriver desired_caps = { "platformName": "Android", "deviceName": "127.0.0.1:7555", "appPackage": "com.example.app", "appActivity": ".MainActivity", "noReset": True, "automationName": "UiAutomator2" } driver = webdriver.Remote("http://127.0.0.1:4723/wd/hub", desired_caps)这里deviceName填的是模拟器的ADB地址,如果你用的是真机,直接填设备序列号。appPackage和appActivity可以用adb shell dumpsys window windows | grep mCurrentFocus查,也可以让开发提供。
3.2 元素定位方式与常用操作封装
元素定位是整个脚本最关键的环节。Appium支持多种定位策略,我日常用得比较多的是id、class name、xpath、accessibility id。原生控件一般都有resource-id,定位时可以优先用id,稳定而且快。如果id没有,再用xpath。但xpath性能不好,尤其是层数很深的页面,我一般会尽量精简表达式,避免全量遍历。
写脚本的时候,把常用的用户操作封装成方法。比如点击、输入、等待元素出现、断言元素是否存在:
def wait_element(driver, locator, timeout=10): from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC wait = WebDriverWait(driver, timeout) return wait.until(EC.presence_of_element_located(locator)) def input_text(driver, locator, text): element = wait_element(driver, locator) element.click() element.clear() element.send_keys(text) def assert_element_visible(driver, locator): assert wait_element(driver, locator) is not None使用起来就像这样:
login_btn = ("id", "com.example:id/btn_login") input_text(driver, ("id", "com.example:id/et_username"), "testuser") input_text(driver, ("id", "com.example:id/et_password"), "123456") driver.find_element(*login_btn).click() assert_element_visible(driver, ("id", "com.example:id/tv_home"))这里要注意,find_element本身不会重试,加上WebDriverWait才能避免页面加载慢导致偶发失败。
3.3 等待策略与稳定性优化
页面自动化的难点不是操作,而是时序。拿到元素之前,页面可能还没渲染完。我用得最多的是显式等待,给每个关键操作设置一个超时时间,比如10秒。虽然隐式等待也能设,但它对整个driver的find_element都生效,和显式等待混在一起会引发不确定的行为,所以我自己很少用。强制等待time.sleep也尽量不要用,除了等待固定动画,其余都是定时炸弹,要么太短要么太长。
一个比较稳妥的做法是:启动App后先等首屏元素出现,再往下操作。如果发现页面有动态加载,可以用轮询元素是否存在。安卓的弹窗权限也值得专门处理,比如首次启动会弹定位、通知权限,处理不好脚本就会中断。我一般会提前在App里关掉这些引导,或者在脚本开头统一点击“允许”。
另外,截图是很好的排错手段。脚本里在每次关键步骤后调用driver.save_screenshot("screenshots/step_x.png"),失败时能立刻知道卡在哪一步。Appium还提供driver.page_source,可以拿到当前页面的控件树,调试定位表达式时特别有用。
4. 框架化改造与批量执行
4.1 用pytest组织自动化用例
单个脚本跑通只是开始,真正要落地还得把脚本工程化。我习惯用pytest做用例管理和执行。先把项目目录整理成:testcases/、pages/、data/、utils/、report/。pages目录放页面对象的封装,比如LoginPage、HomePage,每个页面一个类,类里放这个页面的元素和操作。testcases目录只放业务用例,通过调用page方法去组装。
数据驱动用pytest的parametrize很合适:
import pytest @pytest.mark.parametrize("username,password,expected_home", [ ("testuser", "123456", True), ("baduser", "123456", False), ]) def test_login(username, password, expected_home): login_page.login(username, password) assert login_page.is_login_success() == expected_home需要每个用例独立启动App的话,可以用conftest.py里的fixture,将driver的初始化和销毁抽出来,这样不同用例之间互不干扰。
4.2 日志、报告与CI自动执行
测试跑起来之后,报告和日志非常重要。我会在utils/log.py里配置logging,把每个步骤的执行时间、操作结果输出到日志文件。然后结合pytest-html或allure-pytest生成报告。Allure的报告可视化程度高,有每个测试的截图、步骤、参数,调试的时候信息非常足。
如果团队里有CI系统,可以让自动化测试在代码提交后自动触发。流程一般是:拉取代码 -> 安装依赖 -> 启动Appium Server -> 开启模拟器 -> pytest执行用例 -> 上传Allure报告。我在HoRain云上测试时,其实就是把整套脚本放在云上跑,不用占自己电脑的资源,云端的设备也可以随时换。
4.3 多设备并行执行
安卓机型碎片化严重,单测一台设备很难发现兼容问题。我后来引入了多设备并行。前提是每台设备需要一个独立的Appium Server端口,然后在pytest里可以通过pytest-xdist给不同设备分配用例。实际上,更省心的做法是用云测平台的任务分发,比如在HoRain云后台创建多台设备,平台会自动把用例分发到这些设备上,最后汇总报告。如果自己搭,要注意设备间的数据隔离,测试账号不要互相顶掉。
并行时最容易遇到的问题就是资源不够。像我电脑上跑2个模拟器还能接受,4个模拟器CPU直接拉满,这时候云端设备机群的优点就体现出来了。所以我的建议是:开发调试用本地模拟器,回归测试放到云上执行,效率和稳定性都能兼顾。
5. 常见问题与排查技巧实录
5.1 环境类问题排查
环境问题占了我前期踩坑的一大半。最典型的是Appium Server起不来,日志里提示port 4723 already in use,这是因为端口被占了。我一般先执行lsof -i:4723看看是谁占的,然后kill掉旧进程再重启。
adb连接不稳定也会让人头大。模拟器连上后又断开,多半是USB接口供电不稳或者手机锁屏。真机可以关闭自动锁屏,数据线尽量用设备原装,模拟器可以重新adb connect。
| 现象 | 常见原因 | 处理办法 |
|---|---|---|
| Appium启动报App not launched | appPackage或appActivity写错 | 用dumpsys命令确认正确的包名/Activity |
| 元素定位超时 | 页面未加载完 | 调整等待策略,先等首屏元素 |
| 模拟器无法访问网络 | NAT配置问题 | 改用桥接模式或检查宿主机防火墙 |
| 脚本在真机上跑不起来 | USB调试断开 | 检查数据线、开启“仅充电模式下允许调试” |
5.2 脚本稳定性问题
脚本偶尔失败是我最头疼的问题。后来定位到几个规律:页面加载慢导致元素未出现,就用更长的显式等待;控件id变化频繁,就改用相对xpath;系统弹窗遮挡,就在capabilities里加noReset,或写一个统一处理弹窗的函数。还有企业微信、推送通知这类跳动窗口,会在脚本顶层弹出,我会定期扫描页面,如果发现“同意”“知道了”这类文本就先点掉,再继续操作。
另外,一定要把失败截图和日志保存好。我发现很多时候人眼看一下截图,比看几个小时的日志更快定位问题。
5.3 模拟器与真机兼容性问题
模拟器上跑得好好的脚本,换到真机上就瞎了。最常见的是元素尺寸或分辨率不同,导致坐标点击不准确。所以尽量不要用click坐标,而是用真实元素对象点击。如果非要用坐标,也要根据屏幕宽度和高度做比例换算。
不同安卓版本对权限的处理也不同。安卓11以后,部分权限弹窗变成了自动授予,但这反而可能导致App行为变化。我建议测试时尽量覆盖Android 10、11、13几个主要版本,云端设备矩阵其实就是解决这种问题的,一次跑几台,兼容性的底也就探出来了。
6. 最后分享几点个人经验
6.1 学会判断自动化投入产出比
就我自己的实践来说,Python安卓自动化测试最核心的不是写脚本,而是耐心处理环境的飘忽和界面的各种不确定性。很多时候run一次就过了,但过两天又挂了,不能太迷信脚本成功率。要建立一套完整的截图、日志、重跑机制,才能让自动化真正稳定下来。
还有一个容易被忽视的问题:别为了自动化而自动化。一个App如果每周才更新一次,回归用例很少,手动点几下就完了,那自动化的收益确实有限。只有当用例多、重复频繁、又需要多设备回归时,这套Python + Appium的方案才值得投入。想清楚投入产出比,再动手也不迟。
6.2 稳定性靠机制不靠运气
我后来总结了一个原则:脚本里永远不要靠sleep去碰运气。显式等待、失败重试、实时日志,这三样东西缺一不可。我开始跑自动化的时候,总喜欢把timeout设得很短,想着快速失败早点下班,结果大量用例都挂在等待上。后来统一改成10秒起步,关键操作再单独加重试,稳定性一下就上来了。
一个小技巧分享给你:刚开始做自动化,不要追求一次性覆盖很多用例。挑几个跑得最顺的场景先跑起来,等框架稳定后再慢慢加用例。我自己就是从登录、列表滑动、下单三个流程开始,跑顺了才扩到几十条用例。这样好处很明显,每次改动框架,只需要看这几个核心用例能不能过,问题一下就能暴露出来。等跑稳了以后,再往里面添新用例,整个过程会顺畅很多。