刚接触 Appium 时,我总觉得这类工具自带一种“劝退气质”:环境变量、驱动、设备连接、元素定位,哪一环塌了都会让脚本寸步难行。但真正把它用进某个回归项目之后,我又觉得 Appium 其实是性价比很高的一笔投入。它能让手机 App 的界面级测试从“每次发版前人工点两小时”,变成“睡前提交一次任务,早上起来就能拿到一份带截图的失败报告”。这篇内容就是我从零上手 Appium 到能稳定维护一套移动端自动化用例的完整经验记录,适合刚接触 UI 自动化、想快速跑通第一个 Android 脚本的人。
我默认按 Android + Python 路径讲,因为这是最不依赖操作系统、成本最低的组合。只要一条数据线、一台电脑、一部 Android 手机或模拟器,就能跑起来。iOS 部分我会在最后单独说,因为它需要 macOS 和签名环境,门槛确实更高。现在开始,按我实际操作的顺序走一遍环境搭建、用例编写和问题排查,尽量把坑都留在这篇文章里。
1. 移动端自动化到底在解决什么问题
先说需求。很多团队一开始对自动化测试的期待是“全代替手工”,但我的看法是:不要指望它消灭手工测试,而是把重复、机械、容易漏的回归动作交给脚本。一个 App 每两周发一次版,每次都要重测登录、首页、列表、购物流程,这些路径在产品稳定期很少变动,但每轮手工重新点一遍至少消耗半天。更难受的是,人最不擅长做重复检查,点错一个 tab、漏看一条 error toast,问题就被带上线了。
Appium 解决的就是这类问题:它把用户手指点在屏幕上的动作,变成一行行代码里可重复执行的指令。你写好 30 个回归用例,以后每次发版前跑一遍,哪里挂了截图留存、日志可见,效率比人工点按高一个量级。
选 Appium 而不是其他框架,我当时的考量很直接:
- 跨平台:同一套 WebDriver 思路,Android 用 UIAutomator2,iOS 用 XCUITest,脚本结构高度一致;
- 多语言:Python、Java、JavaScript、Ruby、C# 都能写;
- 黑盒视角:它关心的是界面元素和交互行为,不需要 App 源码,适合测试团队独立维护;
- 生态成熟:网上案例多、工具链完整,踩过的坑基本都有人踩过。
当然,如果你要写的是纯性能测试、纯接口测试,Appium 并不是合适选型。它天生适合 UI 行为验证,也就是“用户在真机上怎么操作,脚本就怎么操作”的那一类场景。把定位放清楚,后面学起来就不容易乱。
2. 入门 Appium 前需要弄懂的几个概念
说实话,第一个能让我跑通脚本的不是某个 API,而是把下面三个概念理清了。这也是很多新手卡住的原因——不是代码不会写,而是不知道代码和手机之间到底发生了什么。
2.1 WebDriver 协议与我们常说的“驱动”
Appium 的架构可以理解成“客户端——服务端——驱动——设备”四层。客户端就是你的 Python/Java 测试代码,它通过 HTTP 发送请求;Appium Server 接收这些请求,翻译成对应设备驱动能听懂的命令;驱动再把命令传给手机里的自动化代理,最终由代理去操作控件。
这一整套语义来自 WebDriver 协议。WebDriver 本来是浏览器自动化领域的标准,后来 Appium 把它扩展到了移动端。所以你会发现很多方法名和 Selenium 长得一模一样,比如find_element、click、send_keys。这不是巧合,而是同一个协议下的自然延伸。
用生活里的场景打比方:客户端像一个说普通话的用户,Appium Server 像翻译中心,驱动像各地方言翻译员。你只对翻译中心说普通话,翻译中心再帮你转成当地话,至于手机里真正动手的人是谁,测试代码不需要关心。
2.2 Session 与 Desired Capabilities
启动一次自动化测试,本质上就是建立一个 Session。Session 可以理解成 Appium Server 与设备之间的一条专用通信链路。链路建立时,客户端会带上“这辆车我要这么启动”的配置,这就是 Desired Capabilities。
我早期常犯的错误是把 Capabilities 当成“随便填填”的启动参数。其实它直接决定了 Session 能不能建、App 能不能被正确拉起。我从 Android 最常用的几个给你梳理一下:
| Capability | 作用 | 示例 |
|---|---|---|
| platformName | 平台类型 | Android |
| appium:automationName | 使用哪种驱动 | UiAutomator2 |
| appium:udid | 设备 ID | emulator-5554 |
| appium:app | App 安装包路径 | /path/to/demo.apk |
| appium:noReset | 是否保留 App 数据 | true |
| appium:autoGrantPermissions | 自动授予权限 | true |
看到字段名里带appium:前缀,是 Appium 2 之后的严格区分。标准 WebDriver 能力不强制带前缀,但 Appium 扩展能力必须带,避免和协议标准字段发生冲突。很多人从老教程里复制代码跑不通,往往就是少了这几处前缀。
2.3 自动化脚本真正操作的是什么:元素
界面自动化绕不开一个词:元素。对 Appium 来说,屏幕上能显示的控件大多可以理解为“元素”,比如输入框、按钮、列表项、图片。每个原生控件在运行时都会暴露自己的层级信息,Android 上的界面树可以被 UIAutomator2 驱动读出来,iOS 上则通过可访问性信息暴露给 XCUITest。
自动化测试最核心的动作就是“先找到元素,再对这个元素执行操作”。所以定位元素是不是稳定,直接决定脚本寿命。千万不要用固定坐标去点击,因为屏幕尺寸、分辨率、系统字体一变,坐标就全废了。怀抱这个原则,后面的内容才有意义。
3. 环境搭建:从零到跑起第一个脚本
我见过有人卡在环境搭建两三天,整个人被劝退。其实按顺序来,四个小时足够。下面这条路是自己反复装过几台机器后验证过的,照着做可以少踩很多坑。
3.1 先定技术栈:Python + Appium 2
先说版本策略。Appium 1.x 已经停止维护,新项目直接用 Appium 2。安装 Appium Server 需要 Node.js,因为 Appium 本身就是 Node.js 项目。你先装 Node.js LTS 版,再用 npm 全局安装:
npm install -g appium装完验证一下:
appium --version我目前的机器上装的是 2.x,命令输出会简短地显示版本号。接下来安装 Python 客户端库,我用的是 Appium-Python-Client:
pip install Appium-Python-Client注意这个库要和 Appium Server 的大版本匹配。Appium 2 对应新版客户端,如果你还在用老代码里的appium.webdriver那套写法,很多类已经被重构过了,照着新语法写会是更省心的路。
3.2 安装驱动和启动 Server
Appium 2 最大的变化之一是驱动单独安装。它不像 1.x 把所有驱动都内置好,而是按需下载。Android 原生自动化用到的驱动叫uiautomator2,安装命令:
appium driver install uiautomator2查看已安装驱动:
appium driver list安装完成后启动 Server。最简单的方式是直接输入:
appium默认监听4723端口。如果你想兼容老项目的地址习惯,可以用:
appium --base-path /wd/hub这样客户端请求地址就是http://127.0.0.1:4723/wd/hub。新版默认路径其实是根路径,我看到不少新同学把老教程的地址填进去,结果返回 404。先确认启动日志里输出的 base path 到底是什么,再看客户端地址对应填什么,能少一半的连接问题。
3.3 Android SDK、adb 与测试设备准备
Appium 要连接 Android 设备,靠的是 adb。Android SDK 里的platform-tools包含了 adb,安装完以后,把adb所在目录配置到环境变量 PATH 里。Linux 和 macOS 下通常还要设置 ANDROID_HOME:
export ANDROID_HOME=/path/to/android-sdk export PATH=$PATH:$ANDROID_HOME/platform-toolsWindows 上就在系统环境变量里加,道理一样。接着打开测试设备的“开发者选项”,打开“USB 调试”。用adb devices确认设备被识别:
adb devices如果看到unauthorized,就说明手机上的授权弹窗没点,先在手机上点允许。如果是模拟器,一般启动模拟器后直接就能看到device状态。
拿到设备之后,下一步是准备被测 App。你开发团队手里通常有 APK 文件,用 adb 直接安装:
adb install app-debug.apk注意这里的包名和启动 Activity 后面会用到,别盖太多业务细节,先保证能装能启动即可。
3.4 跑通第一个最小脚本
环境就绪后,先别写复杂的用例,我建议你直接跑一个“启动被测 App 并停住”的最小脚本,验证整条链路通不通。下面这段是我在 Python 里常用的一种写法:
from appium import webdriver from appium.options.android import UiAutomator2Options options = UiAutomator2Options() options.platform_name = "Android" options.automation_name = "UiAutomator2" options.device_name = "emulator-5554" options.app = "/path/to/app-debug.apk" options.no_reset = True options.auto_grant_permissions = True driver = webdriver.Remote("http://127.0.0.1:4723", options=options) print(driver.session_id) driver.quit()device_name并不严格要求等于 adb devices 的序列号,但在有多个设备的时候,最好用udid精确指定。最简单的是启动 Appium Server 后运行这段代码,如果打印出 session_id,说明 Session 建立成功,整条链路已经通了。
提示:第一次启动 UIAutomator2 驱动时,Appium 会在手机上安装两个辅助 APK,用于自动化操作。这需要一点时间,属正常现象,不要让脚本以为设备卡住就着急杀掉。
4. 定位元素和常用交互:核心细节拆解
链路通了,接下来就是日常写用例时最常碰的环节:定位元素。这部分严重影响脚本稳定性,我给它单独拆成几个小节。你会发现,定位思路比代码写法更重要。
4.1 先让 Appium Inspector 告诉我们界面长什么样
写定位器之前,你得先知道当前界面上有哪些控件、属性是什么。Appium 官方提供了一个图形化工具 Appium Inspector,它可以通过已有的 session 连接设备,实时抓取页面结构。
打开 Inspector,配置和你测试代码里一样的 Desired Capabilities,启动会话后,它会展示一张类 XML 的控件树。你可以看到一个按钮可能长这样:
<android.widget.Button resource-id="com.example.demo:id/login_btn" text="登录" content-desc="" bounds="[200,1200][680,1320]" />从这棵树上能直观看到元素 id、文本、描述信息。没有 Inspector 的时候,命令行也有替代方案,比如 UIAutomatorViewer,但体验比 Inspector 差一些。建议你直接装 Appium Inspector,它和 Appium 2 配合更顺。
4.2 定位策略怎么选才不容易“周一挂周五挂”
定位器选得好不好,是脚本维护成本的分水岭。我的选择优先级一般是这样:
- resource-id 优先,因为它通常是开发者定义的稳定标识;
- content-desc 次之,可访问性信息对自动化更友好;
- 组合 class + text 兜底;
- 最后才用 XPath。
为什么不推荐一上来就写 XPath?因为 XPath 依赖界面层级,稍微有点结构调整,路径就变了。更可怕的是大量使用//android.widget.*[contains(@text, "...")]这种模糊写法,一旦页面相似元素较多,定位到错的对象也悄无声息。resource-id 更像是“元素的身份证”,在 Android 上强烈优先用它。
举个例子:
from appium.webdriver.common.appiumby import AppiumBy username_input = driver.find_element(AppiumBy.ID, "com.example.demo:id/username") password_input = driver.find_element(AppiumBy.ID, "com.example.demo:id/password") login_button = driver.find_element(AppiumBy.ID, "com.example.demo:id/login_btn")如果没有 id,再用 text 定位:
password_input = driver.find_element( AppiumBy.XPATH, "//android.widget.EditText[@text='请输入密码']" )这里要特别提醒:text 定位容易受输入内容影响,比如输入密码后 text 属性可能变成你输入的密文,最好只用来定位未输入状态的空控件。
4.3 点击、输入、滑动与长按的写法
元素找到以后,交互方法基本是固定的。点击和输入最常用:
username_input.clear() username_input.send_keys("tester01") password_input.send_keys("123456") login_button.click()clear()看起来简单,但一定要记得用。有些输入框里预填了提示文本,直接send_keys会把内容追加在后面,用例断言很莫名其妙地就失败了。
滑动操作在移动端场景里出现频率很高,比如首页轮播、列表滑动、引导页切换。Appium 2 里可以用移动端扩展指令,比较简洁:
driver.execute_script("mobile: swipe", {"direction": "up", "duration": 500})长按的操作,我会用更明确的坐标形式,但尽量避免依赖绝对坐标,如果你必须用,也是先取元素位置再计算偏移:
from appium.webdriver.common.touch_action import TouchAction # TouchAction 在新版里不推荐,能用元素方法就尽量别用坐标实际上我会倾向用 Selenium 里更通用的 ActionChains,或者直接选择驱动提供的原生 API。这和你引入的辅助库版本有关,原则是能基于元素操作就不基于坐标,能不做绝对像素计算就不做。
4.4 等待的艺术:别再用固定 sleep
新手最喜欢time.sleep(3),因为它“简单直接、好像也没出问题”。但固定等待是脚本不稳定的头号来源:网络慢一点,3 秒不够;网络快了,3 秒又浪费。
Appium 里常见的等待有三种:
- 隐式等待:设置后对每个元素查找都生效;
- 显式等待:对特定条件反复轮询,直到条件满足或超时;
- 强等待:就是 sleep,尽量少用。
我建议以显式等待为主。比如等待登录按钮出现:
from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC wait = WebDriverWait(driver, 20) login_btn = wait.until( EC.presence_of_element_located((AppiumBy.ID, "com.example.demo:id/login_btn")) )等待条件也很有讲究:presence_of_element_located只表示元素出现在 DOM/控件树上,不代表它已经可点击。要处理动画结束、加载完成的场景,最好用element_to_be_clickable这样的条件。
5. 实操全流程:从零写一个登录测试用例
理论说了一堆,不如完整写一个登录用例。这是我带新人时最喜欢用的练手 demo,因为它覆盖了启动 App、定位元素、输入、点击、等待、断言、失败截图这些核心能力。整个过程大概两三小时能跑通。
5.1 测试 App、账号准备与前置条件
第一个问题:为什么要“准备一个可测试的 App”?因为真实项目里的 App 可能依赖生产网络、需要现场登录、或者有复杂的启动广告。自动化测试的前置条件越简单,脚本越可靠。
我当时的做法是让开发提供一个测试环境专用的 debug APK,包名固定成com.example.demo,测试环境配了固定账号tester01 / 123456。同时关闭了启动页广告,这样脚本不用处理那些偶然出现的“跳过按钮”。
如果需要获取被测 App 的包名和初始 Activity,adn 有一些小技巧:
# 启动应用后查看当前焦点窗口信息 adb shell dumpsys window | grep mCurrentFocus输出类似:
mCurrentFocus=Window{... com.example.demo/com.example.demo.MainActivity}有了包名和 Activity 信息,即使不加appium:app路径,也可以让 Appium 直接拉起启动页:
options.app_package = "com.example.demo" options.app_activity = ".MainActivity"这种方式适合 App 已经装好、不想每次重新安装的场景,跑起来更快。
5.2 页面对象模型组织用例
当用例只有一条时,写成脚本毫无问题;当有几十条甚至上百条时,页面对象模型就是必须课。它的核心思想是:把页面元素和操作封装到独立的类里,测试用例只描述业务行为,不关心细节。
一个简单项目结构:
test_demo/ ├── conftest.py ├── pages/ │ ├── login_page.py │ └── home_page.py └── tests/ └── test_login.pyconftest.py里放 driver 的 fixture,保证每个用例都能拿到同一个 session:
import pytest from appium import webdriver from appium.options.android import UiAutomator2Options @pytest.fixture(scope="function") def driver(): options = UiAutomator2Options() options.platform_name = "Android" options.automation_name = "UiAutomator2" options.device_name = "emulator-5554" options.app_package = "com.example.demo" options.app_activity = ".MainActivity" options.no_reset = True driver = webdriver.Remote("http://127.0.0.1:4723", options=options) yield driver driver.quit()login_page.py里封装登录页:
from appium.webdriver.common.appiumby import AppiumBy class LoginPage: def __init__(self, driver): self.driver = driver def login(self, username, password): self.driver.find_element( AppiumBy.ID, "com.example.demo:id/username" ).send_keys(username) self.driver.find_element( AppiumBy.ID, "com.example.demo:id/password" ).send_keys(password) self.driver.find_element( AppiumBy.ID, "com.example.demo:id/login_btn" ).click()用例就变得很简洁:
def test_login_success(driver): from pages.login_page import LoginPage LoginPage(driver).login("tester01", "123456") # 断言登录后跳转到了首页 assert driver.current_activity == ".HomeActivity"这里我故意用了很小的封装,但已经能看到维护上的好处:某天登录按钮的 id 变了,只需要改LoginPage一处,而不是到每个用例里去找。
5.3 断言、截图与失败现场保存
UI 自动化的断言比接口测试要“糙”一些,因为界面状态可能五花八门。最常用的是断言 Activity 名称、断言关键元素是否存在、断言页面标题文本,以及断言某个 toast 提示是否出现。
判断 toast 比较特殊,toast 不是常驻元素,容易出现又消失。我常用一个简化方案:等待一个 XPath 指向 toast 文本,如果能等到,就说明提示出现。
toast_locator = (AppiumBy.XPATH, "//*[@text='账号或密码错误']") try: WebDriverWait(driver, 5).until( EC.presence_of_element_located(toast_locator) ) except Exception: screenshot_path = "/tmp/login_error_{}.png".format(time.time()) driver.save_screenshot(screenshot_path) raise失败现场不只是截一张图。如果条件允许,最好同时抓一下adb logcat,看看 App 在这一步有没有抛异常。截图配合日志,排查问题会快很多。我现在维护的用例里,凡是用例失败,必留三样东西:截图、控件树快照、App 日志。别嫌麻烦,这些都是后来定位问题的救命稻草。
6. 常见问题与排查技巧实录
写 Appium 脚本最花时间的往往不是业务逻辑,而是环境问题和环境引起的不稳定。我自己踩过太多坑,直接把遇到频率最高的症状、原因、对策放在一起,做成一张速查表给你参考。
6.1 我遇到频率最高的 6 类报错
| 现象 | 可能原因 | 排查方法 | 处理方式 |
|---|---|---|---|
| 连接 4723 失败 | Appium Server 没启动 / 端口被占 | 看 Server 日志,检查端口 | 重启 Server 或换端口 |
| session 创建超时 | App 路径不对 / 设备未找到 | 检查 Capabilities 和 adb devices | 先用 adb install 验证 APK 可装 |
| 找不到元素 | 定位器写错 / 页面没加载完成 | 用 Inspector 看当前控件树 | 换定位器,改用显式等待 |
| 输入框内容清不掉 | 没有调用 clear() | 查看元素属性 | 先 clear 再 send_keys |
| 点击无响应 | 元素被遮挡 / 动画未结束 | 看截图 | 等待可点击状态 |
| 设备 unauthorized | 手机没允许 USB 调试 | 看 adb devices 状态 | 在手机上点允许授权 |
第一类“连接失败”是新手区高频悲剧。往往不是你代码有问题,而是 Appium Server 没启动,或者上次启动的进程没退出、端口被占用。Windows 下可以用命令查端口占用:
netstat -ano | findstr 4723找到占用进程的 PID,再用taskkill /PID 进程号 /F处理。macOS/Linux 下则用lsof -i :4723。
6.2 从日志里读 Appium 的潜台词
Appium Server 的日志是最直接的“病历”。默认日志可能有点简略,建议启动时加上 debug 级别:
appium --log-level debug启动后,Server 会打印出 Capabilities 解析结果、查找驱动的过程、设备连接状态。如果你的 session 一直创建不成功,日志里通常会在最后几行丢出一段报错原因。
我见过最典型的例子是 Capabilities 里appium:app指向了一个不存在的 APK 路径,日志就会提示找不到文件。你如果不知道从哪查起,第一次看日志就看三处:是否说明驱动已启动、是否输出了设备信息、是否抛出了堆栈异常。
如果 App 本身崩溃了,光看 Appium 日志还不够,要看设备日志:
adb logcat -c # 复现操作 adb logcat -s AndroidRuntime:E6.3 让脚本稳定一点的小经验
经验这东西比语法难找。下面几条是我在实际维护里逐渐形成的习惯,分享给你:
一是不要在一个用例里塞太多交互步骤。用例如同单元测试,越小越容易定位问题。登录失败,第一步看是不是定位失败;一个购物链路用例挂了,到底垮在哪一步,截图时很难一眼看出。
二是控制noReset的使用场景。noReset=True可以保留 App 数据,登录态会延续,第二次跑用例可能直接跳过了登录页。这对登录用例来说反而是坏事。我的做法是用例依赖什么状态,就在前置条件里主动构造,而不是依赖上次留下的随机状态。
三是给表单类用例准备独立测试账号。千万别拿生产账号去跑自动化,一旦触发风控、验证码策略,用例全挂不说,还可能误伤真实业务数据。我所在的项目专门备了一套“自动化专用账号”,所有场景都靠它跑。
四是关注设备资源。手机内存不足、存储满了,都会导致 App 启动变慢、动画卡顿,最终反馈到用例上就是超时。模拟器长期不清理,也会越来越慢。我每周至少重启一次设备,跑批量任务前先清理缓存,测试稳定性明显提升。
7. 想往 iOS、真机和持续集成扩展怎么办
如果你是刚入门,先把 Android 模拟器跑通就够了。但等到要建设完整的自动化体系,以下几个方向迟早绕不开,提前了解能少走弯路。
iOS 自动化目前只能在 macOS 上跑。Appium 连接 iOS 设备需要安装 XCUITest 驱动:
appium driver install xcuitest真机还要额外处理 WebDriverAgent 的签名,这个环节最容易卡人。我的建议是:如果团队没有专门的 iOS 设备管理环境,先把 Android 侧的用例跑起来,iOS 侧宁可晚一点再上,也不要为了“全平台”把推进节奏拖慢。
真机测试方面,真机和模拟器最大的差异不只是速度,还有权限弹窗、输入法、系统弹窗这些环境因素。真机上务必要做两件事:一是关闭系统自动更新,二是把“充电时保持屏幕唤醒”打开。否则跑着跑着屏幕灭了,脚本直接处于失明状态。
持续集成是另一个大坑。无论用哪个 CI 平台,本质上要回答三个问题:测试环境怎么准备、App 包从哪里来、测试报告怎么沉淀。环境准备建议用 Docker 或独立的测试机,不要跑在开发者日常用的机器上。App 包一般从构建产物里拿,由 CI 在测试前安装。报告至少要有失败截图和日志,否则自动化再稳定,团队也不会有太多信心去信任它。
我自己在扩展过程中最大的感受:自动化工具本身不是重点,重点是把它嵌到稳定的测试流程里。只有脚本能被常态化触发、问题能被及时跟进,投入产出比才真正体现出来。
最后分享一个我很个人的经验:刚学 Appium 时,不要一上来就追求覆盖所有 App 功能,挑一个登录流程反复练,把它写出不同写法的版本,再逐步加等待策略、页面对象封装、失败现场收集。等你回头再看,很多难点其实只是“没见过”。保持动手节奏,比看一百篇教程都管用。