Appium相关
Appium 移动自动化测试 完整系统学习大纲
本文面向测试工程师及移动端测试初学者。Appium 是移动端自动化测试的行业事实标准:一套脚本同时跑 Android 和 iOS,不侵入 App 源码,支持原生应用、混合应用(H5)和移动 Web。与站内已有的 UIAutomator2(Android 专用、Python 直连)相比,Appium 的强项是跨平台 + 生态完整,代价是多一层 Server 通信、执行略慢。两者定位互补,建议都了解。 文中基于 Appium 2.x + Appium-Python-Client 3.x(Appium 1.x 已于 2022 年停止维护),Windows 用户请在 PowerShell(非 cmd)中执行。
一、Appium 概述
1.1 什么是 Appium?
- 定义:Appium 是一个开源、跨平台的移动应用自动化测试框架,用统一的 API 驱动 Android、iOS 上的原生应用、混合应用和移动 Web 应用。
- 出身:2012 年由 Dan Cuellar 发起(最初叫 iOSAuto),2013 年加入 Sauce Labs 并支持 Android,2022 年发布 Appium 2.0(插件化架构,驱动与主体解耦),目前主流版本为 2.x。
- 核心思想:用统一的 WebDriver 协议屏蔽底层差异 —— 测试脚本只管发标准指令,具体怎么操控手机交给底层的各家驱动去适配。
1.2 为什么它是行业标准?
| 优势 | 说明 |
|---|---|
| 跨平台 | 同一套脚本跑 Android + iOS,测试代码复用率可达 70%+ |
| 不侵入源码 | 不需要改 App 代码、不需要重新打包(黑盒测试) |
| 多语言 | Python / Java / JavaScript / Ruby / C# 任选 |
| 支持多种应用形态 | 原生 App、混合 App(WebView)、移动 Web 浏览器 |
| 生态成熟 | Appium Inspector、云测平台(BrowserStack / SauceLabs / Testin)全兼容 |
| 面试硬通货 | 中高级测试岗移动端必问 |
1.3 分层结构:Appium 和底层框架的关系
很多人搞不清 Appium、UIAutomator2、XCUITest 的关系,其实它们是上下层,不是并列关系:
你的测试脚本(Python / Java)
│ 标准 WebDriver 协议(HTTP + JSON)
▼
┌─────────────────────────┐
│ Appium Server (Node) │ 接收指令、管理 Session、转发
└───────────┬─────────────┘
│ 按 platformName 分派给对应 Driver
┌───────────┴────────────┐
▼ ▼
┌───────────────┐ ┌───────────────┐
│ uiautomator2 │ │ xcuitest │ ← Appium 2.x 的可插拔驱动
│ driver │ │ driver │
└───────┬───────┘ └───────┬───────┘
▼ ▼
Android UIAutomator2 iOS XCUITest / WDA
▼ ▼
Android 设备/模拟器 iOS 真机/模拟器
- Appium 本身不直接操作设备,它只是”协议翻译 + 指令转发”。
- Android 侧实际干活的是 Google 官方的 UIAutomator2(
uiautomator2driver);iOS 侧是 Apple 的 XCUITest + WebDriverAgent(xcuitestdriver)。 - 所以 Appium 2.x 装完主体后,必须单独安装驱动(见 2.2),这是 1.x 用户最容易踩的坑。
1.4 与其他移动端框架的对比
| 框架 | 跨平台 | 语言 | 侵入性 | 执行速度 | 适用场景 |
|---|---|---|---|---|---|
| Appium | Android + iOS | 多语言 | 黑盒,不侵入 | 中(有 Server 转发) | 跨平台回归、云测平台、团队统一 |
| UIAutomator2 | 仅 Android | Python | 黑盒 | 快(直连设备) | 纯 Android 的项目、快速脚本 |
| Espresso / XCUITest | 单平台 | Java/Kotlin、Swift | 白盒(需源码) | 最快 | 开发自测、单测级 UI 测试 |
| Airtest(网易) | Android + iOS + 游戏 | Python | 黑盒,图像识别 | 中 | 游戏、Unity、无法定位元素的场景 |
| Playwright | 移动浏览器模拟 | 多语言 | — | 快 | 移动 Web / H5,不能测原生控件 |
一句话:要跨平台选 Appium,要极致速度且只测 Android 选 UIAutomator2,要测游戏或有图像识别需求看 Airtest。 三者不是替代关系,很多团队是 Appium 做主回归 + UIAutomator2 做快速冒烟。
1.5 核心概念
| 概念 | 说明 |
|---|---|
| Client | 你写脚本用的客户端库(如 Appium-Python-Client),负责发 HTTP 请求 |
| Server | Node.js 写的服务(npm install -g appium),监听 4723 端口,管理 Session |
| Driver | 平台适配器(uiautomator2 / xcuitest / espresso 等),Appium 2.x 独立安装 |
| Plugin | 可选插件(如 images 图像定位、relaxed-caps 等) |
| Session | 一次会话 = 一次连接。脚本通过 session 与设备交互,driver.quit() 结束 |
| Capabilities | 启动参数(平台、设备、App、行为开关),决定”连哪台设备、开哪个 App” |
| Desired Capabilities | 旧称,Appium 2.x 起规范叫 W3C Capabilities,需用 Options 类传入 |
1.6 一次点击背后发生了什么?
1. 脚本:driver.find_element(AppiumBy.ID, "login_btn").click()
2. Client → 转成 HTTP POST /session/xxx/element(JSON Wire / W3C 协议)
3. Server → 查 session,转给对应 driver(如 uiautomator2)
4. Driver → 调 Android UIAutomator2 的 API 在设备上找控件
5. 设备 → 返回元素信息,driver 再执行 click
6. 结果沿原路返回给脚本
这也是 Appium 比 UIAutomator2 慢的原因:每次操作都是一次 HTTP 往返。优化方向是减少无效查询(见第九章)。
1.7 适用场景与不适用
适合:
- 跨 Android / iOS 的回归测试(核心场景)
- 需要云测平台批量跑兼容性(几十台机型)
- 混合应用(原生 + WebView)的端到端流程
- 团队技术栈统一、要沉淀 PO 框架
不适合:
- 追求毫秒级响应的性能/启动耗时测试(用原生工具更准)
- 游戏、Unity、Flutter 自绘引擎界面(控件树不可见,图像识别更合适)
- 纯单元测试级别验证(那是 Espresso/XCUITest 的活)
二、环境搭建(Appium 2.x)
2.1 安装 Node.js 与 Appium Server
Appium Server 是 Node.js 程序,先装 Node(建议 LTS 18+):
# 验证 Node 环境
node -v
npm -v
# 全局安装 Appium(2.x)
npm install -g appium
# 验证
appium --version
2.2 安装驱动(Appium 2.x 必做!)
Appium 2.x 把驱动从主体剥离开了,装完 appium 不能直接跑,必须装 driver:
# 查看可安装的驱动
appium driver list
# Android 驱动
appium driver install uiautomator2
# iOS 驱动(需要在 macOS 上才有意义)
appium driver install xcuitest
# 查看已安装
appium driver list --installed
# 更新驱动
appium driver update uiautomator2
⚠️ Appium 1.x 用户最容易懵的地方:1.x 装完
npm install -g appium就能用,2.x 直接跑会报Could not find a driver for automationName 'UiAutomator2'。记住 2.x = 主体 + 驱动。
2.3 安装 Appium Inspector
Appium 2.x 起,Appium Desktop(含 Inspector)已废弃,Inspector 独立成单独应用:
- 下载地址:https://github.com/appium/appium-inspector/releases
- 作用:连上设备后实时查看控件树、录制操作、复制定位表达式
- 相当于 UIAutomator2 生态里的
weditor
2.4 Android 环境(JDK + Android SDK)
- 安装 JDK 11+,配置
JAVA_HOME:$env:JAVA_HOME = "C:\Program Files\Java\jdk-17" [Environment]::SetEnvironmentVariable("JAVA_HOME", $env:JAVA_HOME, "Machine") - 安装 Android Studio 或单独装 command-line tools,配置
ANDROID_HOME:$env:ANDROID_HOME = "$env:LOCALAPPDATA\Android\Sdk" $env:PATH += ";$env:ANDROID_HOME\platform-tools;$env:ANDROID_HOME\emulator" - 验证设备连接:
adb devices # List of devices attached # emulator-5554 device ← 状态必须是 device,不是 unauthorized/offline
真机要点:手机打开开发者选项 → USB 调试,首次连接要在手机上点”允许 USB 调试”。显示
unauthorized就重插并确认授权弹窗。
2.5 iOS 环境(仅 macOS)
| 依赖 | 说明 |
|---|---|
| macOS + Xcode | 必须,iOS 自动化只能在 Mac 上跑(XCUITest 依赖 Xcode 编译) |
| Xcode Command Line Tools | xcode-select --install |
carthage |
WDA 依赖管理:brew install carthage |
ios-deploy / tidevice |
真机部署;tidevice 可在 Linux/Windows 上做部分设备管理 |
| WebDriverAgent 签名 | 真机必须用开发者账号给 WDA 签名(免费账号有效期 7 天) |
brew install carthage
npm install -g ios-deploy
pip3 install tidevice # 跨平台查看设备、安装 App 很方便
tidevice list
2.6 安装 Python 客户端
pip install Appium-Python-Client
pip install pytest # 测试框架
pip install pytest-rerunfailures # 失败重试(稳定性必备)
python -c "import appium; print(appium.__version__)"
2.7 环境与启动参数
# 启动服务(2.x 默认端口 4723,默认 base path 为 / ,不再是 /wd/hub)
appium
# 常用参数
appium --address 127.0.0.1 --port 4723 --log-level info
appium --relaxed-security # 放开 mobile: shell 等受限指令
appium --base-path /wd/hub # 老客户端兼容时需要
Appium 2.x 的 URL 变化:1.x 是
http://127.0.0.1:4723/wd/hub,2.x 默认去掉了/wd/hub,直接写http://127.0.0.1:4723。脚本连不上时先确认这一点。
三、第一个自动化脚本
3.1 常用 Capabilities 速查
| 参数 | 说明 | 示例 |
|---|---|---|
platformName |
平台 | Android / iOS |
automationName |
驱动名 | UiAutomator2 / XCUITest |
deviceName |
设备名(任意,仅标识) | emulator-5554 |
udid |
设备唯一标识(多设备必填) | emulator-5554 / iOS 的 40 位 UUID |
app |
App 安装包路径或 URL | D:/app/demo.apk |
appPackage / appActivity |
Android 启动入口 | com.demo / .MainActivity |
bundleId |
iOS 包名 | com.demo.app |
noReset |
不清除 App 数据(保留登录态) | true |
fullReset |
卸载重装 | false |
unicodeKeyboard / resetKeyboard |
中文输入支持 | true |
autoAcceptAlerts |
自动接受系统弹窗(iOS) | true |
autoGrantPermissions |
自动授予权限(Android) | true |
newCommandTimeout |
无指令超时自动结束 session | 300 |
systemPort |
uiautomator2 driver 端口(多机并发必配) | 8200 |
chromedriverExecutableDir |
指定 chromedriver 目录(WebView 用) | D:/drivers |
settings[waitForIdleTimeout] |
等待界面空闲超时(毫秒) | 100 |
3.2 Android 示例
from appium import webdriver
from appium.options.android import UiAutomator2Options
from appium.webdriver.common.appiumby import AppiumBy
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
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
options.auto_grant_permissions = True
options.new_command_timeout = 300
driver = webdriver.Remote("http://127.0.0.1:4723", options=options)
try:
# 等待并点击登录按钮
login_btn = WebDriverWait(driver, 15).until(
EC.element_to_be_clickable((AppiumBy.ID, "com.example.demo:id/btn_login"))
)
login_btn.click()
# 输入账号密码
driver.find_element(by=AppiumBy.ID, value="com.example.demo:id/et_account").send_keys("test01")
driver.find_element(by=AppiumBy.ID, value="com.example.demo:id/et_pwd").send_keys("123456")
driver.find_element(by=AppiumBy.ID, value="com.example.demo:id/btn_submit").click()
# 断言:出现首页元素
WebDriverWait(driver, 10).until(
EC.presence_of_element_located((AppiumBy.ID, "com.example.demo:id/rv_home"))
)
print("登录流程通过")
finally:
driver.quit() # 必须退出,否则 session 残留占用设备
3.3 iOS 示例
from appium import webdriver
from appium.options.ios import XCUITestOptions
options = XCUITestOptions()
options.platform_name = "iOS"
options.automation_name = "XCUITest"
options.platform_version = "17.0"
options.device_name = "iPhone 15"
options.udid = "00008120-XXXXXXXXXXXXXXXX"
options.bundle_id = "com.example.demo"
options.no_reset = True
options.auto_accept_alerts = True
driver = webdriver.Remote("http://127.0.0.1:4723", options=options)
driver.quit()
3.4 Session 生命周期与常见启动失败
创建 session(能力校验 → 装驱动到设备 → 启动 App → 返回 sessionId)
↓
执行操作(find / click / swipe ...)
↓
driver.quit() ← 关键:释放设备、清理端口
| 报错 | 原因 | 处理 |
|---|---|---|
Could not find a driver for automationName |
没装 driver | appium driver install uiautomator2 |
Could not find a connected Android device |
adb 看不到设备 | adb devices 检查授权与连接 |
Failed to launch App / Activity not found |
包名或 Activity 写错 | 用 adb shell dumpsys window \| findstr mCurrentFocus 查当前 Activity |
Session not created after 60s |
设备卡死 / WDA 未启动 | 重启 adb(adb kill-server && adb start-server)、重启设备 |
Original error: Unable to find an active device |
模拟器未启动 | 先 emulator -avd <name> 或启动 Android Studio 设备 |
四、元素定位(核心重点)
4.1 定位策略全表
| 方式 | 写法 | 说明 |
|---|---|---|
| ID(推荐) | AppiumBy.ID, "com.demo:id/btn_login" |
Android 用 resource-id,iOS 用 name |
| Accessibility ID(推荐) | AppiumBy.ACCESSIBILITY_ID, "登录" |
Android content-desc / iOS label,跨平台统一 |
| ClassName | AppiumBy.CLASS_NAME, "android.widget.Button" |
同类元素多,通常配合索引 |
| XPath | AppiumBy.XPATH, "//*[@text='登录']" |
万能但慢且脆弱,慎用 |
| Android UIAutomator | AppiumBy.ANDROID_UIAUTOMATOR, 'new UiSelector().text("登录")' |
Android 最强,支持组合、滚动查找 |
| iOS Predicate | AppiumBy.IOS_PREDICATE, "name == '登录' AND visible == 1" |
iOS 属性表达式,强大 |
| iOS Class Chain | AppiumBy.IOS_CLASS_CHAIN, "**/XCUIElementTypeButton[\name == ‘登录’`]”` |
iOS 层级定位,性能优于 XPath |
| Image(图像) | AppiumBy.IMAGE, base64图片 |
需 appium plugin install images,适合游戏 |
4.2 定位优先级建议
1. resource-id / accessibility id ← 最稳定、最快
2. Android UIAutomator / iOS Predicate ← 平台原生强力语法
3. 组合定位(父子、兄弟、instance) ← 无 id 时的次选
4. XPath ← 最后手段(层级一变就废)
5. 坐标 / 图像 ← 实在没招才用,极不稳定
4.3 Appium Inspector 用法
- 启动 Appium Server,手机连好电脑。
- 打开 Appium Inspector,填 Remote Host
127.0.0.1、Port4723、Remote Path/(2.x 默认)。 - Desired Capabilities 里填平台、设备、包名等,点 Start Session。
- 左侧是屏幕截图,中间是控件树,右侧是选中元素的属性。
- 选中元素后可一键复制 ID / XPath 等表达式,直接粘到脚本里。
技巧:Inspector 的控件树是当前屏幕快照,页面跳转后要点刷新按钮重新抓取。
4.4 组合与层级定位
# Android:多条件组合(UiSelector 链式调用)
driver.find_element(
by=AppiumBy.ANDROID_UIAUTOMATOR,
value='new UiSelector().resourceId("com.demo:id/item").text("设置")'
)
# Android:父子关系
driver.find_element(
by=AppiumBy.ANDROID_UIAUTOMATOR,
value='new UiSelector().className("android.widget.LinearLayout")'
'.childSelector(new UiSelector().text("确定"))'
)
# Android:按索引取第 N 个
items = driver.find_elements(by=AppiumBy.ID, value="com.demo:id/item_title")
items[2].click()
# XPath:相对路径 + 属性组合
driver.find_element(
by=AppiumBy.XPATH,
value='//android.widget.ListView//android.widget.TextView[@text="我的订单"]'
)
# iOS:Predicate
driver.find_element(
by=AppiumBy.IOS_PREDICATE,
value="type == 'XCUIElementTypeButton' AND name CONTAINS '登录'"
)
4.5 为什么不建议滥用 XPath?
- 慢:XPath 要遍历整棵控件树,Appium 上尤其明显(每次可能几百毫秒)。
- 脆:UI 层级一调整就失效,维护成本高。
- 同步问题:长列表里 XPath 依赖绝对位置,滑动后就找错了。
经验法则:能用 resource-id 就别用 XPath。确实要用就写成相对路径
//*[@text='xxx'],避免/hierarchy/android.widget.FrameLayout/.../...这种绝对路径。
五、元素操作与手势
5.1 基础操作
el = driver.find_element(by=AppiumBy.ID, value="com.demo:id/et_search")
el.click() # 点击
el.send_keys("Appium") # 输入
el.clear() # 清空
text = el.text # 取文本
enabled = el.get_attribute("enabled") # 取属性(enabled/checked/selected/clickable...)
bounds = el.rect # 取位置和尺寸 {"x":0,"y":100,"width":200,"height":80}
displayed = el.is_displayed() # 是否可见
5.2 滑动与滚动
Appium 2.x 推荐用 W3C 手势指令(mobile: 脚本方式),老版 TouchAction 已废弃:
def swipe_up(driver, percent=0.6):
"""整体上滑"""
size = driver.get_window_size()
driver.execute_script("mobile: swipeGesture", {
"left": size["width"] * 0.1,
"top": size["height"] * 0.2,
"width": size["width"] * 0.8,
"height": size["height"] * 0.6,
"direction": "up",
"percent": percent,
"speed": 2000
})
def scroll_to_text(driver, text):
"""Android:滚动直到某文本出现(UiScrollable,最稳)"""
return driver.find_element(
by=AppiumBy.ANDROID_UIAUTOMATOR,
value=f'new UiScrollable(new UiSelector().scrollable(true).instance(0))'
f'.scrollIntoView(new UiSelector().text("{text}"));'
)
def iOS_scroll(driver, direction="down"):
"""iOS:滚动手势"""
driver.execute_script("mobile: scroll", {"direction": direction})
5.3 长按、拖拽、双击
# 长按
driver.execute_script("mobile: longClickGesture", {
"elementId": el.id, "duration": 1500
})
# 拖拽(把 A 拖到 B)
driver.execute_script("mobile: dragGesture", {
"elementId": el_a.id, "endX": 200, "endY": 600, "speed": 1500
})
# 双击
driver.execute_script("mobile: doubleClickGesture", {"elementId": el.id})
5.4 系统按键、截图、通知栏
driver.press_keycode(4) # Android 返回键(KEYCODE_BACK)
driver.press_keycode(3) # Home 键
driver.press_keycode(187) # 最近任务
driver.open_notifications() # 打开通知栏(Android)
driver.save_screenshot("screen.png") # 截图
driver.get_screenshot_as_base64() # 转 base64,用于测试报告
5.5 应用管理
driver.install_app("D:/app/demo.apk")
driver.remove_app("com.demo")
driver.activate_app("com.demo") # 启动/切前台
driver.terminate_app("com.demo") # 强杀
driver.background_app(5) # 退后台 5 秒再回来
driver.is_app_installed("com.demo")
driver.start_activity("com.demo", ".SettingActivity") # 直接进指定页面
driver.current_package # 当前包名
driver.current_activity # 当前 Activity(Android)
5.6 混合应用:WebView 切换
混合 App(原生壳 + H5 页面)必须先切换上下文才能定位网页元素:
# 1. 查看所有上下文
print(driver.contexts) # ['NATIVE_APP', 'WEBVIEW_com.demo']
# 2. 切到 WebView
driver.switch_to.context("WEBVIEW_com.demo")
# 3. 用 Web 的定位方式操作 H5
driver.find_element(by=AppiumBy.CSS_SELECTOR, value="#username").send_keys("test")
driver.find_element(by=AppiumBy.XPATH, value="//button[@id='submit']").click()
# 4. 切回原生
driver.switch_to.context("NATIVE_APP")
⚠️ 常见坑:
- App 的 WebView 必须打开 debug 模式(开发需在 WebView 上设
setWebContentsDebuggingEnabled(true)),否则contexts里看不到 WEBVIEW。- chromedriver 版本必须匹配手机里的 Chrome/WebView 版本,不匹配会报
chromedriver ... not supported。用chromedriverExecutableDir指定对应版本目录。- 真机上 WebView 包名可能是
WEBVIEW_com.demo,也可能是WEBVIEW_chrome,要以driver.contexts实际输出为准。
六、等待机制
6.1 三种等待对比
| 类型 | 写法 | 优劣 |
|---|---|---|
| 强制等待 | time.sleep(3) |
简单但死板,拖慢整体耗时 |
| 隐式等待 | driver.implicitly_wait(10) |
全局生效,只管”找到元素”,不管”可点击” |
| 显式等待(推荐) | WebDriverWait(driver, 15).until(...) |
精准、可设条件,稳定性首选 |
建议:显式等待为主 + 少量隐式等待,不要混用过多(两者叠加会导致超时时间难以预期)。
6.2 移动端常用等待条件
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
wait = WebDriverWait(driver, 15)
wait.until(EC.presence_of_element_located((AppiumBy.ID, "com.demo:id/title"))) # 存在
wait.until(EC.visibility_of_element_located((AppiumBy.ID, "com.demo:id/title"))) # 可见
wait.until(EC.element_to_be_clickable((AppiumBy.ID, "com.demo:id/btn"))) # 可点击
wait.until(EC.invisibility_of_element_located((AppiumBy.ID, "com.demo:id/loading"))) # 消失
wait.until(lambda d: d.find_element(by=AppiumBy.ID, value="com.demo:id/tv").text == "成功") # 自定义
6.3 自定义等待:等页面稳定
def wait_page_stable(driver, timeout=10, interval=0.5):
"""连续两次截图像素一致,认为页面不再刷新(对抗转圈动画)"""
import time
prev = driver.get_screenshot_as_base64()
end = time.time() + timeout
while time.time() < end:
time.sleep(interval)
curr = driver.get_screenshot_as_base64()
if curr == prev:
return True
prev = curr
return False
def wait_toast(driver, text, timeout=8):
"""等待 Toast 出现并返回元素"""
return WebDriverWait(driver, timeout).until(
EC.presence_of_element_located((AppiumBy.XPATH, f'//android.widget.Toast[contains(@text, "{text}")]'))
)
6.4 超时相关参数
| 参数 | 作用 |
|---|---|
newCommandTimeout |
多少秒没收到指令就自动结束 session(防设备长期占用) |
settings[waitForIdleTimeout] |
等待界面空闲的超时,调小可提速(默认 10000ms) |
settings[waitForSelectorTimeout] |
查找元素的超时(默认 10000ms) |
WebDriverWait(driver, n) |
脚本级显式等待 |
七、Page Object 模式与工程化
与站内
Details/下的 PO 模式专题配套,这里给 Appium 的完整落地结构。
7.1 目录结构
appium_project/
├── config/
│ ├── caps.yaml # 设备与能力配置
│ └── config.yaml # 环境、账号数据
├── pages/
│ ├── base_page.py # 基类:通用操作封装
│ ├── login_page.py # 登录页
│ └── home_page.py # 首页
├── tests/
│ ├── conftest.py # pytest fixture:driver 生命周期
│ └── test_login.py
├── reports/ # 截图与报告
└── requirements.txt
7.2 基类封装
# pages/base_page.py
import os, time
from appium.webdriver.common.appiumby import AppiumBy
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
class BasePage:
def __init__(self, driver, timeout=15):
self.driver = driver
self.wait = WebDriverWait(driver, timeout)
def find(self, locator, timeout=None):
wait = self.wait if timeout is None else WebDriverWait(self.driver, timeout)
return wait.until(EC.presence_of_element_located(locator))
def click(self, locator):
self.wait.until(EC.element_to_be_clickable(locator)).click()
def input(self, locator, text):
el = self.find(locator)
el.clear()
el.send_keys(text)
def text_of(self, locator):
return self.find(locator).text
def is_exist(self, locator, timeout=3):
try:
self.find(locator, timeout)
return True
except Exception:
return False
def swipe_up(self, percent=0.6):
size = self.driver.get_window_size()
self.driver.execute_script("mobile: swipeGesture", {
"left": size["width"] * 0.1, "top": size["height"] * 0.2,
"width": size["width"] * 0.8, "height": size["height"] * 0.6,
"direction": "up", "percent": percent, "speed": 2000
})
def scroll_to_text(self, text):
return self.driver.find_element(
by=AppiumBy.ANDROID_UIAUTOMATOR,
value=f'new UiScrollable(new UiSelector().scrollable(true).instance(0))'
f'.scrollIntoView(new UiSelector().text("{text}"));'
)
def screenshot(self, name="failure"):
path = os.path.join("reports", f"{name}_{int(time.time())}.png")
os.makedirs("reports", exist_ok=True)
self.driver.save_screenshot(path)
return path
7.3 页面对象
# pages/login_page.py
from appium.webdriver.common.appiumby import AppiumBy
from pages.base_page import BasePage
class LoginPage(BasePage):
ACCOUNT = (AppiumBy.ID, "com.demo:id/et_account")
PASSWORD = (AppiumBy.ID, "com.demo:id/et_pwd")
SUBMIT = (AppiumBy.ID, "com.demo:id/btn_submit")
TOAST = (AppiumBy.XPATH, "//android.widget.Toast")
def login(self, username, password):
self.input(self.ACCOUNT, username)
self.input(self.PASSWORD, password)
self.click(self.SUBMIT)
return self
def get_toast(self):
return self.text_of(self.TOAST)
7.4 pytest fixture(含失败截图)
# tests/conftest.py
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.demo"
options.app_activity = ".MainActivity"
options.no_reset = True
d = webdriver.Remote("http://127.0.0.1:4723", options=options)
yield d
d.quit()
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call):
"""用例失败时自动截图"""
outcome = yield
report = outcome.get_result()
if report.when == "call" and report.failed:
d = item.funcargs.get("driver")
if d:
d.save_screenshot(f"reports/{item.name}.png")
# tests/test_login.py
from pages.login_page import LoginPage
def test_login_success(driver):
page = LoginPage(driver)
page.login("test01", "123456")
assert page.is_exist((LoginPage.TOAST[0], "com.demo:id/rv_home"))
def test_login_wrong_password(driver):
page = LoginPage(driver)
page.login("test01", "wrong")
assert "密码错误" in page.get_toast()
7.5 多设备并发
# 每台设备独占:Appium Server 端口 + uiautomator2 systemPort + 设备 udid
DEVICES = [
{"udid": "emulator-5554", "server": "http://127.0.0.1:4723", "system_port": 8200},
{"udid": "emulator-5556", "server": "http://127.0.0.1:4725", "system_port": 8201},
]
def build_driver(dev):
options = UiAutomator2Options()
options.platform_name = "Android"
options.automation_name = "UiAutomator2"
options.udid = dev["udid"]
options.system_port = dev["system_port"]
options.app_package = "com.demo"
options.app_activity = ".MainActivity"
return webdriver.Remote(dev["server"], options=options)
# 启动两个 Appium Server
appium -p 4723 &
appium -p 4725 &
# 用 pytest-xdist 并发
pip install pytest-xdist
pytest -n 2 tests/
⚠️ 并发三要素:Server 端口不同、
systemPort不同、udid不同。任一重复都会导致 session 抢占失败。
八、特殊场景处理
8.1 Toast 提示
# Android:UIAutomator2 驱动直接支持 Toast 定位
toast = WebDriverWait(driver, 8).until(
EC.presence_of_element_located((AppiumBy.XPATH, '//android.widget.Toast'))
)
print(toast.text)
# 指定内容
driver.find_element(by=AppiumBy.XPATH, value='//android.widget.Toast[contains(@text,"登录成功")]')
Toast 存活时间短(2~3 秒),必须用显式等待而非 sleep 后查找;iOS 无原生 Toast,通常是对话框或自定义 View,用
ACCESSIBILITY_ID定位。
8.2 系统弹窗与权限弹窗
# 方案1:启动参数自动授权(推荐)
options.auto_grant_permissions = True # Android
options.auto_accept_alerts = True # iOS 系统弹窗
# 方案2:脚本里按文本点掉(更可控,可处理"仅在使用中允许")
for text in ["仅在使用中允许", "允许", "始终允许"]:
if driver.find_elements(by=AppiumBy.XPATH, value=f'//*[@text="{text}"]'):
driver.find_element(by=AppiumBy.XPATH, value=f'//*[@text="{text}"]').click()
break
# 方案3:iOS 弹窗显式接受
driver.switch_to.alert.accept()
8.3 中文输入
options.unicode_keyboard = True
options.reset_keyboard = True
说明:这两个参数会安装并使用 Appium 的输入法来完成中文输入。若遇到输入乱码或输入法切不回来,可改用
driver.execute_script("mobile: shell", {"command": "input text ..."}),或让开发在控件上加content-desc,用send_keys前先clear()。
8.4 其他高频场景
| 场景 | 做法 |
|---|---|
| 横竖屏切换 | driver.orientation = "LANDSCAPE" |
| 深层页面直达 | driver.start_activity(pkg, activity) 跳过前置步骤,提速 |
| 清理 App 数据 | 去掉 noReset 或 driver.remove_app + install_app |
| 验证码 | 让测试环境配置万能码 / 后端接口取码 / 关闭校验,不要靠 OCR |
| 上传图片 | 通过 push_file 把文件推到设备相册再从相册选 |
| 弱网测试 | 借助 adb shell 改网络策略或配合 Fiddler/Charles 限速 |
| 应用升级测试 | 装旧版本跑一遍 → install_app 升级包 → 再跑一遍 |
九、稳定性与性能优化
9.1 用例 flaky(时好时坏)的常见原因
| 原因 | 表现 | 对策 |
|---|---|---|
| 用坐标 / 绝对路径 XPath | 换机型就失败 | 改 resource-id、相对定位 |
| 等待不足 | 偶发 NoSuchElement | 显式等待 + 页面稳定判断 |
| 动画/转圈未结束 | 点击落空 | 关动画(开发者选项)、waitForIdleTimeout 调小 |
| 前置数据污染 | 用例互相影响 | 用例独立、noReset + 接口造数 |
| 设备状态残留 | 上条用例弹窗未关 | teardown 里 terminate_app 或重启 App |
| 并发端口冲突 | 随机 session 失败 | 端口按设备隔离 |
9.2 失败重试
pip install pytest-rerunfailures
pytest tests/ --reruns 2 --reruns-delay 3
9.3 执行速度优化
# 关掉等待空闲(提速最明显的一项,默认 10000ms)
driver.update_settings({"waitForIdleTimeout": 100})
# 只抓取必要层级,减少控件树传输量
driver.update_settings({"snapshotMaxDepth": 50})
driver.update_settings({"ignoreUnimportantViews": True})
# 尽量复用 session:用 noReset=True + start_activity 直达目标页
9.4 云测平台
需要覆盖几十上百款机型时,本地跑不现实,可把同一套 Appium 脚本交给云测平台:
- BrowserStack / SauceLabs:海外,机型全,付费
- Testin / 阿里云测 / 腾讯 WeTest:国内,真机租赁
- 接入方式通常只是把
webdriver.Remote的地址换成平台提供的 URL + 加鉴权 cap,脚本本身不用改
十、常见问题排查(FAQ)
1. Session 创建失败 / 报 driver 找不到
按顺序查:
appium driver list --installed确认装了uiautomator2(2.x 必装)。adb devices状态是device,不是unauthorized/offline。- Appium Server 日志里看真实报错(
appium --log-level debug)。 - 确认 URL 没有多写
/wd/hub(2.x 默认不需要)。 - 模拟器完整启动(看到桌面,不是黑屏)。
2. 元素明明在屏幕上却找不到
- 在 WebView 里:
print(driver.contexts),切到WEBVIEW_xxx再找。 - 页面已跳转:Inspector 刷新一下,或重新
driver.page_source看看当前树。 - 被弹窗/遮罩挡住:先处理弹窗,或用
is_displayed()判断可见性。 - 长列表未加载:先滚动到底触发加载,再查找。
- 用了绝对路径 XPath:层级变了,改成相对路径或 ID。
3. Appium 1.x 迁移到 2.x 要注意什么?
| 1.x | 2.x |
|---|---|
| 装完即用 | 需 appium driver install uiautomator2 |
地址带 /wd/hub |
默认不带,写成 http://host:4723 |
desired_capabilities={...} 字典 |
用 UiAutomator2Options() 对象 |
driver.find_element_by_id(...) |
driver.find_element(by=AppiumBy.ID, value=...) |
| Appium Desktop 自带 Inspector | Inspector 独立安装 |
appium-doctor 常用 |
官方弱化,逐项自查为主 |
4. 中文输入失败或乱码
- 加
unicodeKeyboard=True+resetKeyboard=True。 - 确认设备上的 Appium 输入法被启用(设置 → 输入法里能看到)。
- 仍不行就用
adb shell input text兜底,或让开发给控件加content-desc。 - 结束后注意把输入法切回,否则手工测试时会觉得”键盘不对”。
5. WebView 切不过去 / chromedriver 报错
driver.contexts打印看看有没有WEBVIEW_,没有就是 App 没开 debug 模式,找开发打开setWebContentsDebuggingEnabled(true)。- 报错
This version of ChromeDriver only supports Chrome version xx→ chromedriver 版本不匹配:- 手机设置里看 Android System WebView 版本;
- 下载对应 chromedriver,用
chromedriverExecutableDir指定目录。
- 部分 App 用的是自研内核(如 X5),需要
driver.execute_script或让开发切换内核。
6. 多设备并发互相干扰
- 每台设备起一个独立 Appium Server(不同
--port)。 - 每个 driver 指定独立
systemPort(uiautomator2 默认 8200,会冲突)。 - 必须传
udid,不要只靠deviceName。 - 测试结束后
driver.quit(),否则下次连接会报设备忙。
十一、面试高频题
| 问题 | 答题要点 |
|---|---|
| Appium 原理? | Client 发 WebDriver 协议请求 → Appium Server(Node)→ 按平台分派 driver(uiautomator2 / xcuitest)→ 底层调原生框架 → 设备上执行 |
| 和 UIAutomator2 什么区别? | Appium 是跨平台框架,Android 底层就是 UIAutomator2;直接调 UIAutomator2 更快但仅限 Android |
| 为什么跨平台? | 统一 WebDriver 协议 + 可插拔 driver,屏蔽底层差异;脚本层用 accessibility id 等通用定位方式 |
| Appium 2.x 有什么变化? | 驱动/插件与主体解耦、需单独 install;base path 去掉 /wd/hub;W3C capabilities(Options 类);find_element_by_* 废弃 |
| 元素找不到怎么排查? | 看 context、刷新控件树、处理弹窗、检查是否长列表未加载、改相对定位 |
| 怎么保证用例稳定? | 显式等待、稳定定位方式、用例独立、失败重试、关动画、页面稳定判断 |
| 混合应用怎么测? | driver.contexts 查看,切 WEBVIEW_xxx,chromedriver 版本要匹配,App 需开 WebView debug |
| 多设备并发怎么做? | 多 Server 端口 + 独立 systemPort + 指定 udid + pytest-xdist |
十二、学习路径建议
第一阶段(跑通)
概念分层 → 环境搭建(Node/驱动/SDK)→ 第一个脚本 → Inspector 看控件树
第二阶段(写稳)
定位策略 → 手势与应用管理 → WebView 切换 → 等待机制
第三阶段(工程化)
PO 模式封装 → pytest fixture → 失败截图 → 多设备并发 → 报告与集成
第四阶段(进阶)
稳定性治理 → 云测平台 → CI 集成 → 性能与兼容性专项
一句话总结:Appium 的价值不在”能点按钮”,而在”一套脚本跨两个平台、还能上云测平台规模化”。 学的时候抓住三条主线:协议分层(理解原理)、定位与等待(决定稳定性)、PO 封装(决定可维护性),其余都是这三条的延伸。