Appium自动化测试完全指南1
第一章:Appium基础入门
1.1 Appium概述与架构原理
1.1.1 什么是Appium
Appium是一个开源的、跨平台的移动应用自动化测试框架,它允许开发者使用相同的API对iOS、Android和Windows平台上的原生应用、移动Web应用和混合应用进行自动化测试。Appium的设计理念是"无需重新编译应用即可测试",这使得它成为移动自动化测试领域的首选工具。
1.1.2 C/S架构详解
Appium采用经典的客户端/服务器(Client/Server)架构,这种设计使得测试代码与被测应用完全解耦。
架构组件说明:
| 组件 | 职责 | 说明 |
|---|---|---|
| 客户端 | 发送WebDriver命令 | 支持多种编程语言,提供统一的API |
| Appium Server | 接收并处理请求 | Node.js实现的HTTP服务器,监听客户端请求 |
| Driver Manager | 管理驱动程序 | Appium 2.0新增组件,负责驱动的安装和管理 |
| Driver | 执行平台特定命令 | 将WebDriver命令转换为平台特定的自动化指令 |
| 设备层 | 运行被测应用 | 真实设备或模拟器 |
1.1.3 Session机制
Session是Appium的核心概念,每个自动化测试会话都通过Session来管理:
Session生命周期代码示例:
from appium import webdriver
# 定义Capabilities配置
capabilities = {
'platformName': 'Android', # 指定平台为Android
'deviceName': 'emulator-5554', # 设备名称
'automationName': 'UiAutomator2', # 使用UiAutomator2驱动
'appPackage': 'com.example.app', # 应用包名
'appActivity': '.MainActivity' # 启动Activity
}
# 创建Session - 建立与Appium Server的连接
driver = webdriver.Remote(
command_executor='http://127.0.0.1:4723', # Appium Server地址
desired_capabilities=capabilities # 会话配置
)
# 获取Session ID - 每个会话的唯一标识
session_id = driver.session_id
print(f"当前Session ID: {session_id}")
# 执行测试操作...
# Session在此期间保持活跃状态
# 关闭Session - 清理资源并断开连接
driver.quit()
1.1.4 WebDriver协议详解
Appium基于WebDriver协议,经历了从JSON Wire Protocol到W3C WebDriver协议的演进:
JSON Wire Protocol vs W3C WebDriver协议对比:
| 特性 | JSON Wire Protocol | W3C WebDriver |
|---|---|---|
| 标准化 | Selenium社区规范 | W3C官方标准 |
| Capabilities格式 | desiredCapabilities |
capabilities.alwaysMatch |
| 元素定位 | findElement |
findElement (支持CSS选择器) |
| Actions API | 有限支持 | 完整的W3C Actions支持 |
| 错误处理 | 自定义错误码 | 标准化错误响应 |
W3C WebDriver协议请求示例:
import requests
import json
# W3C格式的Capabilities
capabilities = {
"capabilities": {
"alwaysMatch": {
"platformName": "Android",
"appium:deviceName": "emulator-5554",
"appium:automationName": "UiAutomator2",
"appium:appPackage": "com.example.app",
"appium:appActivity": ".MainActivity"
},
"firstMatch": [{}]
}
}
# 发送创建Session的HTTP请求
response = requests.post(
'http://127.0.0.1:4723/session',
headers={'Content-Type': 'application/json'},
data=json.dumps(capabilities)
)
# 解析响应
session_data = response.json()
print(f"Session创建结果: {json.dumps(session_data, indent=2)}")
1.2 Appium 2.0核心变化
Appium 2.0是一次重大版本更新,带来了架构层面的根本性变化。
1.2.1 Driver独立管理
在Appium 1.x中,所有Driver都内置在Appium中;而Appium 2.0将Driver独立出来,实现了按需安装:
Driver管理命令:
# 查看已安装的Driver
appium driver list
# 查看可用的Driver
appium driver list --installed
# 安装指定Driver
appium driver install uiautomator2
appium driver install xcuitest
appium driver install espresso
appium driver install flutter
# 更新Driver
appium driver update uiautomator2
# 卸载Driver
appium driver uninstall uiautomator2
1.2.2 插件机制
Appium 2.0引入了插件系统,允许扩展Appium的功能:
# 查看可用插件
appium plugin list
# 安装插件
appium plugin install images # 图像识别插件
appium plugin install device-farm # 设备农场插件
appium plugin install execute-driver # 批量执行插件
# 启动Appium时加载插件
appium --use-plugins=images,device-farm
1.2.3 Base Path变化
Appium 2.0的默认Base Path从/wd/hub变为/:
# Appium 1.x 连接方式
driver = webdriver.Remote(
'http://127.0.0.1:4723/wd/hub', # 需要包含/wd/hub路径
desired_capabilities=capabilities
)
# Appium 2.0 连接方式
driver = webdriver.Remote(
'http://127.0.0.1:4723', # 直接使用根路径
options=options
)
# 如果需要兼容旧路径,启动时指定
# appium --base-path=/wd/hub
1.3 Driver类型详解
1.3.1 UiAutomator2 Driver
UiAutomator2是Android平台的主要驱动,基于Google的UiAutomator2框架:
工作原理:
- Driver将两个APK安装到设备上(server和server.test)
- 通过Android Accessibility Service获取元素信息
- 使用UiAutomator2 API执行操作
适用场景:
- Android 5.0及以上版本
- 原生应用测试
- 混合应用测试
- 需要跨应用操作的场景
1.3.2 XCUITest Driver
XCUITest是iOS平台的主要驱动,基于Apple的XCUITest框架:
WebDriverAgent(WDA)说明:
- Apple提供的XCUITest测试框架的封装
- 需要在iOS设备上运行
- 提供HTTP接口供Appium调用
适用场景:
- iOS 9.3及以上版本
- 原生应用测试
- 混合应用测试
- 真机和模拟器都支持
1.3.3 Espresso Driver
Espresso Driver是Android平台的高性能驱动:
# Espresso Driver配置
capabilities = {
'platformName': 'Android',
'automationName': 'Espresso', # 使用Espresso驱动
'appPackage': 'com.example.app',
'appActivity': '.MainActivity',
'espressoBuildConfig': { # Espresso构建配置
'additionalAppDependencies': [
'androidx.test.espresso:espresso-contrib:3.4.0'
]
}
}
Espresso vs UiAutomator2对比:
| 特性 | UiAutomator2 | Espresso |
|---|---|---|
| 执行速度 | 较慢 | 快 |
| 跨应用支持 | 支持 | 不支持 |
| 稳定性 | 一般 | 高 |
| 学习成本 | 低 | 中 |
| 适用场景 | 通用测试 | 白盒测试 |
1.3.4 Flutter Driver
Flutter Driver专门用于Flutter应用测试:
# Flutter Driver配置
capabilities = {
'platformName': 'Android',
'automationName': 'Flutter', # 使用Flutter驱动
'appPackage': 'com.example.flutter_app',
'appActivity': '.MainActivity'
}
# Flutter特有的元素查找方式
element = driver.find_element('flutter', 'key_name')
1.4 UiAutomator2与XCUITest工作原理
1.4.1 UiAutomator2工作原理
核心组件:
- UiAutomator2 Server APK:运行在设备上的服务端
- Accessibility Service:获取屏幕元素信息
- ADB Bridge:与设备通信的桥梁
1.4.2 XCUITest工作原理
1.5 环境搭建
1.5.1 Node.js环境安装
# Windows使用安装包或nvm-windows
# 下载地址: https://nodejs.org/
# macOS使用Homebrew
brew install node
# Linux使用包管理器
# Ubuntu/Debian
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
sudo apt-get install -y nodejs
# 验证安装
node --version # 应显示v18.x.x或更高版本
npm --version # 应显示npm版本号
1.5.2 Appium服务端安装
# 全局安装Appium 2.x
npm install -g appium
# 验证安装
appium --version
# 安装Appium Doctor(环境诊断工具)
npm install -g appium-doctor
# 运行环境诊断
appium-doctor --android # 检查Android环境
appium-doctor --ios # 检查iOS环境
1.5.3 Driver安装
# 安装Android驱动
appium driver install uiautomator2
# 安装iOS驱动
appium driver install xcuitest
# 验证安装
appium driver list --installed
1.5.4 客户端库安装
# Python客户端
pip install Appium-Python-Client
# 验证安装
python -c "from appium import webdriver; print('Appium Python Client安装成功')"
<!-- Maven依赖 - Java客户端 -->
<dependency>
<groupId>io.appium</groupId>
<artifactId>java-client</artifactId>
<version>9.0.0</version>
</dependency>
// JavaScript客户端
npm install appium
// 或使用webdriverio
npm install webdriverio
1.6 Appium Desktop与Appium Inspector工具
1.6.1 Appium Desktop
Appium Desktop是Appium的图形化界面版本,提供了可视化的操作界面:
主要功能:
- 一键启动/停止Appium Server
- 可视化配置Capabilities
- 内置Appium Inspector
- 实时日志查看
1.6.2 Appium Inspector
Appium Inspector是独立的元素检查工具:
# 启动Appium Inspector需要的配置
{
"platformName": "Android",
"appium:deviceName": "emulator-5554",
"appium:automationName": "UiAutomator2",
"appium:appPackage": "com.example.app",
"appium:appActivity": ".MainActivity"
}
Inspector功能详解:
| 功能 | 说明 | 使用场景 |
|---|---|---|
| 元素树展示 | 显示页面元素的层级结构 | 理解元素关系 |
| 属性查看 | 显示元素的所有属性 | 获取定位信息 |
| 定位器生成 | 自动生成各种定位器 | 快速编写脚本 |
| 交互测试 | 直接在Inspector中操作元素 | 验证定位正确性 |
| 截图功能 | 保存当前屏幕截图 | 文档记录 |
1.7 Appium Doctor环境诊断工具
Appium Doctor可以帮助诊断环境配置问题:
# 运行完整诊断
appium-doctor
# 只检查Android相关配置
appium-doctor --android
# 只检查iOS相关配置
appium-doctor --ios
# 输出示例
# info AppiumDoctor Appium Doctor v1.16.0
# info AppiumDoctor ### Diagnostic for necessary dependencies starting ###
# ✔ ANDROID_HOME is set to "/Users/xxx/Library/Android/sdk"
# ✔ JAVA_HOME is set to "/Library/Java/JavaVirtualMachines/jdk-17.jdk/Contents/Home"
# ✔ adb exists at /Users/xxx/Library/Android/sdk/platform-tools/adb
# ✔ android exists at /Users/xxx/Library/Android/sdk/tools/android
# ✔ emulator exists at /Users/xxx/Library/Android/sdk/emulator/emulator
常见问题及解决方案:
| 问题 | 原因 | 解决方案 |
|---|---|---|
| ANDROID_HOME未设置 | 环境变量未配置 | 添加ANDROID_HOME到系统环境变量 |
| JAVA_HOME未设置 | JDK未正确安装 | 安装JDK并配置JAVA_HOME |
| adb not found | Android SDK未安装 | 安装Android SDK |
| Carthage not found | iOS依赖缺失 | brew install carthage |
1.8 Android模拟器配置与加速
1.8.1 Android模拟器创建
# 使用avdmanager创建模拟器
avdmanager create avd \
-n "TestEmulator" \ # 模拟器名称
-k "system-images;android-33;google_apis;x86_64" \ # 系统镜像
-d "pixel_6" # 设备配置
# 启动模拟器
emulator -avd TestEmulator
# 后台启动模拟器
emulator -avd TestEmulator -no-skin -no-audio -no-window &
1.8.2 硬件加速配置
Windows - HAXM配置:
# 检查HAXM支持
# 需要Intel CPU支持VT-x
# 下载并安装HAXM
# https://github.com/intel/haxm/releases
# 验证HAXM安装
sc query intelhaxm
Windows - Hyper-V配置(推荐):
# 启用Hyper-V(需要Windows 10/11 Pro)
Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V -All
# 或通过控制面板启用
# 控制面板 -> 程序 -> 启用或关闭Windows功能 -> Hyper-V
macOS - Hypervisor.Framework:
# macOS自动使用Hypervisor.Framework
# 无需额外配置
# 检查加速是否生效
emulator -accel-check
# 应显示: accel: 0
1.8.3 GPU加速配置
# 启动模拟器时指定GPU模式
emulator -avd TestEmulator -gpu host # 使用主机GPU
emulator -avd TestEmulator -gpu swiftshader # 软件渲染
emulator -avd TestEmulator -gpu guest # 客户端渲染
1.8.4 内存优化
# 创建模拟器时指定内存大小
avdmanager create avd \
-n "TestEmulator" \
-k "system-images;android-33;google_apis;x86_64" \
-c 2048M \ # SD卡大小
--sdcard 512M # SD卡
# 或编辑config.ini文件
# ~/.android/avd/TestEmulator.avd/config.ini
hw.ramSize=4096
vm.heapSize=256
1.9 iOS模拟器配置与管理
1.9.1 Xcode命令行工具
# 安装Xcode命令行工具
xcode-select --install
# 查看已安装的模拟器
xcrun simctl list devices
# 查看可用的iOS运行时
xcrun simctl list runtimes
# 创建模拟器
xcrun simctl create "iPhone 14" "iPhone 14" "iOS16.2"
# 启动模拟器
xcrun simctl boot "iPhone 14"
open -a Simulator
# 关闭模拟器
xcrun simctl shutdown "iPhone 14"
# 删除模拟器
xcrun simctl delete "iPhone 14"
1.9.2 模拟器管理脚本
import subprocess
import json
class IOSSimulatorManager:
"""iOS模拟器管理类"""
def __init__(self):
self.devices = self._get_devices()
def _get_devices(self):
"""获取所有模拟器设备列表"""
result = subprocess.run(
['xcrun', 'simctl', 'list', 'devices', '-j'],
capture_output=True,
text=True
)
return json.loads(result.stdout)
def boot_device(self, device_name):
"""启动指定模拟器"""
subprocess.run(['xcrun', 'simctl', 'boot', device_name])
subprocess.run(['open', '-a', 'Simulator'])
def shutdown_device(self, device_name):
"""关闭指定模拟器"""
subprocess.run(['xcrun', 'simctl', 'shutdown', device_name])
def install_app(self, device_udid, app_path):
"""安装应用到模拟器"""
subprocess.run(['xcrun', 'simctl', 'install', device_udid, app_path])
def launch_app(self, device_udid, bundle_id):
"""启动应用"""
subprocess.run(['xcrun', 'simctl', 'launch', device_udid, bundle_id])
# 使用示例
simulator = IOSSimulatorManager()
simulator.boot_device("iPhone 14")
1.10 Appium服务端命令行参数详解
1.10.1 基础参数
# 启动Appium Server
appium
# 指定监听地址和端口
appium --address 0.0.0.0 --port 4723
# 指定Base Path
appium --base-path /wd/hub
# 指定日志级别
appium --log-level debug
# 输出日志到文件
appium --log /path/to/appium.log
# 日志带时间戳
appium --log-timestamp
# 本地时区时间戳
appium --local-timezone
1.10.2 会话管理参数
# 允许覆盖已有Session
appium --session-override
# 保持连接超时时间(毫秒)
appium --keep-alive-timeout 60000
# 新命令超时时间(秒)
appium --command-timeout 60
# 连接超时时间(毫秒)
appium --connect-timeout 30000
1.10.3 安全参数
# 允许跨域请求
appium --allow-cors
# 放宽安全限制(允许更多高级命令)
appium --relaxed-security
# 禁止某些不安全的操作
appium --deny-insecure <feature>
# 例如: appium --deny-insecure get_server_logs
1.10.4 完整启动示例
# 生产环境推荐配置
appium \
--address 0.0.0.0 \
--port 4723 \
--base-path / \
--log-level info \
--log /var/log/appium/appium.log \
--log-timestamp \
--local-timezone \
--session-override \
--keep-alive-timeout 60000 \
--allow-cors \
--use-plugins images,device-farm
1.11 Appium REST API详解
1.11.1 API端点概览
1.11.2 常用API示例
import requests
import json
class AppiumRestClient:
"""Appium REST API客户端"""
def __init__(self, base_url='http://127.0.0.1:4723'):
self.base_url = base_url
self.session_id = None
def get_status(self):
"""获取Appium Server状态"""
response = requests.get(f'{self.base_url}/status')
return response.json()
def create_session(self, capabilities):
"""创建新会话"""
payload = {
'capabilities': {
'alwaysMatch': capabilities,
'firstMatch': [{}]
}
}
response = requests.post(
f'{self.base_url}/session',
headers={'Content-Type': 'application/json'},
data=json.dumps(payload)
)
result = response.json()
self.session_id = result.get('value', {}).get('sessionId')
return result
def find_element(self, using, value):
"""查找元素"""
url = f'{self.base_url}/session/{self.session_id}/element'
payload = {'using': using, 'value': value}
response = requests.post(
url,
headers={'Content-Type': 'application/json'},
data=json.dumps(payload)
)
return response.json()
def click_element(self, element_id):
"""点击元素"""
url = f'{self.base_url}/session/{self.session_id}/element/{element_id}/click'
response = requests.post(url)
return response.json()
def send_keys(self, element_id, text):
"""输入文本"""
url = f'{self.base_url}/session/{self.session_id}/element/{element_id}/value'
payload = {'text': text}
response = requests.post(
url,
headers={'Content-Type': 'application/json'},
data=json.dumps(payload)
)
return response.json()
def delete_session(self):
"""删除会话"""
if self.session_id:
requests.delete(f'{self.base_url}/session/{self.session_id}')
self.session_id = None
# 使用示例
client = AppiumRestClient()
# 获取服务器状态
status = client.get_status()
print(f"服务器状态: {status}")
# 创建会话
capabilities = {
'platformName': 'Android',
'appium:deviceName': 'emulator-5554',
'appium:automationName': 'UiAutomator2',
'appium:appPackage': 'com.example.app',
'appium:appActivity': '.MainActivity'
}
session = client.create_session(capabilities)
# 查找元素并操作
element = client.find_element('id', 'com.example.app:id/login_button')
element_id = element['value']['ELEMENT']
client.click_element(element_id)
# 清理会话
client.delete_session()
1.12 会话管理机制
1.12.1 Session复用
from appium import webdriver
from appium.options.common.base import AppiumOptions
class SessionManager:
"""Session管理类,支持会话复用"""
def __init__(self):
self.driver = None
self.session_id = None
def create_session(self, capabilities):
"""创建新会话"""
options = AppiumOptions()
for key, value in capabilities.items():
options.set_capability(key, value)
self.driver = webdriver.Remote(
'http://127.0.0.1:4723',
options=options
)
self.session_id = self.driver.session_id
return self.driver
def attach_to_session(self, session_id, capabilities):
"""连接到已存在的会话"""
options = AppiumOptions()
for key, value in capabilities.items():
options.set_capability(key, value)
# 使用已有Session ID连接
self.driver = webdriver.Remote(
'http://127.0.0.1:4723',
options=options,
direct_connection=True
)
# 手动设置Session ID
self.driver.session_id = session_id
self.session_id = session_id
return self.driver
def get_session_details(self):
"""获取会话详情"""
if self.driver:
return {
'session_id': self.driver.session_id,
'capabilities': self.driver.capabilities
}
return None
def close_session(self):
"""关闭会话"""
if self.driver:
self.driver.quit()
self.driver = None
self.session_id = None
# 使用示例
manager = SessionManager()
# 创建新会话
driver = manager.create_session({
'platformName': 'Android',
'appium:deviceName': 'emulator-5554',
'appium:automationName': 'UiAutomator2'
})
# 保存Session ID供后续使用
saved_session_id = manager.session_id
print(f"Session ID: {saved_session_id}")
# 后续可以复用Session
# manager.attach_to_session(saved_session_id, capabilities)
1.13 Appium并发限制与解决方案
1.13.1 并发限制说明
核心限制:
- 一个Appium Server实例只能处理一个Session
- 多设备并行测试需要多个Appium Server实例
- 每个Server需要使用不同的端口
1.13.2 多Server实例方案
import subprocess
import time
from appium import webdriver
class MultiServerManager:
"""多Appium Server管理类"""
def __init__(self):
self.servers = {} # 存储Server进程信息
def start_server(self, port, device_udid):
"""启动Appium Server实例"""
# 构建启动命令
cmd = [
'appium',
'--port', str(port),
'--base-path', '/wd/hub',
'--log', f'/tmp/appium_{port}.log'
]
# 启动进程
process = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
# 等待Server启动
time.sleep(3)
# 存储Server信息
self.servers[port] = {
'process': process,
'port': port,
'device_udid': device_udid
}
return f'http://127.0.0.1:{port}/wd/hub'
def stop_server(self, port):
"""停止指定Server"""
if port in self.servers:
self.servers[port]['process'].terminate()
del self.servers[port]
def stop_all_servers(self):
"""停止所有Server"""
for port in list(self.servers.keys()):
self.stop_server(port)
# 并行测试示例
def parallel_test(device_config):
"""在单个设备上执行测试"""
driver = webdriver.Remote(
device_config['server_url'],
options=device_config['options']
)
try:
# 执行测试逻辑
driver.find_element('id', 'com.example.app:id/button').click()
# ...
finally:
driver.quit()
# 使用示例
manager = MultiServerManager()
# 为每个设备启动独立的Server
devices = [
{'udid': 'emulator-5554', 'port': 4723},
{'udid': 'emulator-5556', 'port': 4724},
{'udid': 'emulator-5558', 'port': 4725},
]
server_urls = []
for device in devices:
url = manager.start_server(device['port'], device['udid'])
server_urls.append(url)
# 使用多线程执行并行测试
from concurrent.futures import ThreadPoolExecutor
with ThreadPoolExecutor(max_workers=len(devices)) as executor:
futures = []
for i, device in enumerate(devices):
config = {
'server_url': server_urls[i],
'options': {
'platformName': 'Android',
'appium:udid': device['udid'],
'appium:automationName': 'UiAutomator2'
}
}
futures.append(executor.submit(parallel_test, config))
# 清理
manager.stop_all_servers()
1.13.3 Selenium Grid方案
# docker-compose.yml - Selenium Grid配置
version: '3'
services:
selenium-hub:
image: selenium/hub:4.15.0
container_name: selenium-hub
ports:
- "4442-4444:4442-4444"
environment:
- GRID_MAX_SESSION=10
- GRID_BROWSER_TIMEOUT=300
- GRID_TIMEOUT=300
appium-android:
image: appium/appium:2.0.0
depends_on:
- selenium-hub
environment:
- SE_EVENT_BUS_HOST=selenium-hub
- SE_EVENT_BUS_PUBLISH_PORT=4442
- SE_EVENT_BUS_SUBSCRIBE_PORT=4443
- APPIUM_PORT=4723
ports:
- "4723:4723"
volumes:
- /dev/bus/usb:/dev/bus/usb
privileged: true
# 使用Selenium Grid执行测试
from appium import webdriver
from appium.options.android import UiAutomator2Options
def create_driver_with_grid():
"""通过Grid创建Driver"""
options = UiAutomator2Options()
options.platform_name = 'Android'
options.device_name = 'emulator-5554'
options.automation_name = 'UiAutomator2'
options.app_package = 'com.example.app'
options.app_activity = '.MainActivity'
# 连接到Selenium Grid
driver = webdriver.Remote(
command_executor='http://localhost:4444/wd/hub',
options=options
)
return driver
1.14 模拟器与真机环境差异
1.14.1 主要差异对比
| 特性 | 模拟器 | 真机 |
|---|---|---|
| 性能 | 较慢,依赖主机性能 | 真实性能表现 |
| 网络环境 | 虚拟网络,可能有限制 | 真实网络环境 |
| 传感器 | 模拟数据 | 真实传感器 |
| 系统弹窗 | 较少 | 厂商定制弹窗多 |
| 稳定性 | 较稳定 | 受设备状态影响 |
| 成本 | 低 | 高(设备采购维护) |
| 覆盖度 | 系统版本有限 | 可覆盖各种机型 |
1.14.2 真机特殊处理
# 真机测试需要额外处理的场景
class RealDeviceHandler:
"""真机测试特殊处理类"""
def __init__(self, driver):
self.driver = driver
def handle_system_dialogs(self):
"""处理厂商定制系统弹窗"""
# 华为设备权限弹窗
try:
huawei_allow = self.driver.find_element(
'id', 'com.huawei.systemmanager:id/btn_allow'
)
huawei_allow.click()
except:
pass
# 小米设备权限弹窗
try:
xiaomi_allow = self.driver.find_element(
'id', 'com.miui.securitycenter:id/permission_allow_button'
)
xiaomi_allow.click()
except:
pass
# OPPO设备权限弹窗
try:
oppo_allow = self.driver.find_element(
'id', 'com.coloros.permcontroller:id/permission_allow_button'
)
oppo_allow.click()
except:
pass
def handle_keyboard(self):
"""处理真机输入法问题"""
# 隐藏键盘
try:
self.driver.hide_keyboard()
except:
pass
# 切换到英文输入法(某些中文输入法可能导致问题)
self.driver.execute_script(
'mobile: shell',
{'command': 'settings put secure default_input_method com.android.inputmethod.latin/.LatinIME'}
)
def handle_network_switch(self):
"""处理网络切换"""
# 真机可能需要处理网络切换延迟
import time
self.driver.set_network_connection(4) # 切换到数据网络
time.sleep(2) # 等待网络稳定
1.15 Appium 1.x到2.x版本迁移指南
1.15.1 Breaking Changes
| 变更项 | Appium 1.x | Appium 2.x |
|---|---|---|
| Driver管理 | 内置所有Driver | 需单独安装 |
| Base Path | /wd/hub |
/ |
| Capabilities | desiredCapabilities |
capabilities.alwaysMatch |
| 命令行参数 | 部分参数已移除 | 新增插件相关参数 |
| API | 部分API已废弃 | 遵循W3C标准 |
1.15.2 迁移代码示例
# Appium 1.x 代码
from appium import webdriver
# 旧版Capabilities格式
desired_caps = {
'platformName': 'Android',
'deviceName': 'emulator-5554',
'appPackage': 'com.example.app',
'appActivity': '.MainActivity'
}
# 旧版连接方式
driver = webdriver.Remote(
'http://127.0.0.1:4723/wd/hub', # 包含/wd/hub
desired_capabilities=desired_caps
)
# ============================================
# Appium 2.x 代码
from appium import webdriver
from appium.options.android import UiAutomator2Options
# 新版Options格式
options = UiAutomator2Options()
options.platform_name = 'Android'
options.device_name = 'emulator-5554'
options.app_package = 'com.example.app'
options.app_activity = '.MainActivity'
# 新版连接方式
driver = webdriver.Remote(
'http://127.0.0.1:4723', # 不包含/wd/hub
options=options
)
1.15.3 迁移检查清单
## Appium 2.x 迁移检查清单
### 环境准备
- [ ] 卸载Appium 1.x
- [ ] 安装Appium 2.x
- [ ] 安装所需Driver(uiautomator2、xcuitest等)
- [ ] 安装所需插件
### 代码修改
- [ ] 更新客户端库到最新版本
- [ ] 修改Server URL(移除/wd/hub)
- [ ] 更新Capabilities格式
- [ ] 检查废弃的API调用
- [ ] 更新TouchAction为W3C Actions
### 测试验证
- [ ] 运行冒烟测试
- [ ] 验证所有功能正常
- [ ] 检查日志输出
- [ ] 性能对比测试
1.16 第一个测试脚本
1.16.1 完整示例脚本
"""
Appium自动化测试第一个脚本
演示基本的登录流程测试
"""
from appium import webdriver
from appium.options.android import UiAutomator2Options
from appium.webdriver.common.appiumby import AppiumBy
import time
import unittest
class FirstAppiumTest(unittest.TestCase):
"""第一个Appium测试类"""
@classmethod
def setUpClass(cls):
"""测试类初始化 - 创建Driver"""
# 配置UiAutomator2选项
options = UiAutomator2Options()
options.platform_name = 'Android'
options.device_name = 'emulator-5554'
options.automation_name = 'UiAutomator2'
options.app_package = 'com.example.loginapp'
options.app_activity = '.MainActivity'
options.no_reset = False # 每次启动前重置应用状态
options.new_command_timeout = 300 # 命令超时时间(秒)
# 创建Driver实例
cls.driver = webdriver.Remote(
'http://127.0.0.1:4723',
options=options
)
# 设置隐式等待时间
cls.driver.implicitly_wait(10)
def test_login_flow(self):
"""测试登录流程"""
# 步骤1:等待应用启动并检查是否在登录页面
print("步骤1:验证登录页面加载")
login_title = self.driver.find_element(
AppiumBy.ID,
'com.example.loginapp:id/login_title'
)
self.assertTrue(login_title.is_displayed(), "登录页面未正确加载")
# 步骤2:输入用户名
print("步骤2:输入用户名")
username_input = self.driver.find_element(
AppiumBy.ID,
'com.example.loginapp:id/username_input'
)
username_input.clear() # 清空输入框
username_input.send_keys('testuser') # 输入用户名
# 步骤3:输入密码
print("步骤3:输入密码")
password_input = self.driver.find_element(
AppiumBy.ID,
'com.example.loginapp:id/password_input'
)
password_input.clear()
password_input.send_keys('password123')
# 步骤4:隐藏键盘
print("步骤4:隐藏键盘")
try:
self.driver.hide_keyboard()
except:
pass # 键盘可能已经隐藏
# 步骤5:点击登录按钮
print("步骤5:点击登录按钮")
login_button = self.driver.find_element(
AppiumBy.ID,
'com.example.loginapp:id/login_button'
)
login_button.click()
# 步骤6:验证登录成功
print("步骤6:验证登录成功")
# 等待主页元素出现
time.sleep(2) # 等待页面跳转
welcome_text = self.driver.find_element(
AppiumBy.ID,
'com.example.loginapp:id/welcome_text'
)
self.assertIn('testuser', welcome_text.text, "登录失败或欢迎信息不正确")
print("测试完成:登录流程验证成功")
def test_element_attributes(self):
"""演示获取元素属性"""
# 获取元素文本
element = self.driver.find_element(
AppiumBy.ID,
'com.example.loginapp:id/login_button'
)
# 获取各种属性
print(f"元素文本: {element.text}")
print(f"元素是否可见: {element.is_displayed()}")
print(f"元素是否可点击: {element.is_enabled()}")
print(f"元素位置: {element.location}")
print(f"元素大小: {element.size}")
# 获取自定义属性
content_desc = element.get_attribute('contentDescription')
print(f"contentDescription: {content_desc}")
@classmethod
def tearDownClass(cls):
"""测试类清理 - 关闭Driver"""
if cls.driver:
# 截图保存(可选)
screenshot_path = '/tmp/test_screenshot.png'
cls.driver.save_screenshot(screenshot_path)
print(f"截图已保存到: {screenshot_path}")
# 关闭Driver
cls.driver.quit()
print("Driver已关闭")
if __name__ == '__main__':
# 运行测试
unittest.main(verbosity=2)
1.16.2 脚本执行流程图
第二章:Desired Capabilities全面详解
2.1 通用Capabilities参数
2.1.1 平台与设备参数
from appium.options.common.base import AppiumOptions
# 通用Capabilities配置
options = AppiumOptions()
# ========== 平台配置 ==========
# platformName: 指定测试平台
# 取值: 'Android', 'iOS', 'Windows'
# 必填参数
options.set_capability('platformName', 'Android')
# platformVersion: 指定平台版本
# 示例: '13', '16.2'
# 用于确保测试在特定版本上执行
options.set_capability('appium:platformVersion', '13')
# deviceName: 设备名称
# Android: 可以是任意字符串,但建议使用实际设备名
# iOS: 必须使用正确的设备名称
options.set_capability('appium:deviceName', 'Pixel 6')
# udid: 设备唯一标识符
# Android: adb devices获取
# iOS: instruments -s devices获取
# 多设备测试时必须指定
options.set_capability('appium:udid', 'emulator-5554')
2.1.2 自动化引擎参数
# automationName: 指定自动化引擎
# Android可选值: 'UiAutomator2'(默认), 'Espresso', 'Flutter'
# iOS可选值: 'XCUITest'(默认), 'Flutter'
options.set_capability('appium:automationName', 'UiAutomator2')
2.1.3 应用管理参数
# ========== 应用配置 ==========
# app: 应用安装包路径
# 支持: .apk(Android), .app/.ipa(iOS)
# 可以是本地路径或远程URL
options.set_capability('appium:app', '/path/to/app.apk')
# appPackage: Android应用包名
# 用于指定要测试的应用
# 可通过adb shell dumpsys window | grep mCurrentFocus获取
options.set_capability('appium:appPackage', 'com.example.app')
# appActivity: Android应用启动Activity
# 可通过adb shell dumpsys package <package_name>获取
options.set_capability('appium:appActivity', '.MainActivity')
# appWaitActivity: 等待的Activity
# 当启动Activity与实际显示Activity不同时使用
options.set_capability('appium:appWaitActivity', '.SplashActivity')
# bundleId: iOS应用Bundle ID
# 用于指定iOS应用
options.set_capability('appium:bundleId', 'com.example.app')
2.1.4 会话行为参数
# ========== 会话行为配置 ==========
# newCommandTimeout: 命令超时时间(秒)
# 默认60秒,超过此时间无命令则关闭Session
# 设为0禁用超时
options.set_capability('appium:newCommandTimeout', 300)
# noReset: 是否保留应用状态
# True: 不重置应用状态,保留数据和权限设置
# False: 每次启动前重置应用(默认)
options.set_capability('appium:noReset', True)
# fullReset: 是否完全重置
# True: 卸载应用、清除数据、重新安装
# False: 不完全重置(默认)
# 注意: fullReset优先级高于noReset
options.set_capability('appium:fullReset', False)
# autoLaunch: 是否自动启动应用
# True: Session创建后自动启动应用(默认)
# False: 需手动调用launch_app()
options.set_capability('appium:autoLaunch', True)
# forceAppLaunch: 强制启动应用
# 当应用已运行时是否重新启动
options.set_capability('appium:forceAppLaunch', True)
2.1.5 显示与语言参数
# ========== 显示配置 ==========
# orientation: 屏幕方向
# 取值: 'PORTRAIT'(竖屏), 'LANDSCAPE'(横屏)
options.set_capability('appium:orientation', 'PORTRAIT')
# isHeadless: 无头模式
# True: 不显示模拟器界面(CI环境常用)
# False: 显示模拟器界面(默认)
options.set_capability('appium:isHeadless', True)
# ========== 语言配置 ==========
# language: 应用语言
# 取值: ISO 639-1语言代码,如'en', 'zh', 'ja'
options.set_capability('appium:language', 'zh')
# locale: 应用区域设置
# 取值: ISO 3166-1国家代码,如'CN', 'US', 'JP'
options.set_capability('appium:locale', 'CN')
2.1.6 浏览器测试参数
# ========== 浏览器测试配置 ==========
# browserName: 指定浏览器
# Android: 'Chrome', 'Browser'(系统浏览器)
# iOS: 'Safari'
# 设置此参数后不需要设置app/appPackage
options.set_capability('browserName', 'Chrome')
# acceptInsecureCerts: 接受不安全的SSL证书
# True: 接受自签名证书
# False: 拒绝不安全证书(默认)
options.set_capability('acceptInsecureCerts', True)
# pageLoadStrategy: 页面加载策略
# 'normal': 等待页面完全加载(默认)
# 'eager': 等待DOM加载完成
# 'none': 不等待页面加载
options.set_capability('pageLoadStrategy', 'normal')
# unhandledPromptBehavior: 未处理弹窗的默认行为
# 'dismiss': 关闭弹窗
# 'accept': 接受弹窗
# 'dismiss and notify': 关闭并通知
# 'accept and notify': 接受并通知
# 'ignore': 忽略
options.set_capability('unhandledPromptBehavior', 'dismiss')
2.2 Android专属Capabilities
2.2.1 应用启动参数
from appium.options.android import UiAutomator2Options
options = UiAutomator2Options()
# ========== 应用启动配置 ==========
# appPackage: 应用包名(必填)
options.app_package = 'com.example.app'
# appActivity: 启动Activity(必填)
options.app_activity = '.MainActivity'
# appWaitActivity: 等待的Activity
# 当应用启动后跳转到其他Activity时使用
options.app_wait_activity = '.HomeActivity'
# appWaitPackage: 等待的包名
# 跨应用启动时使用
options.app_wait_package = 'com.example.target'
# appWaitDuration: 等待Activity启动的超时时间(毫秒)
# 默认20000ms
options.app_wait_duration = 30000
# androidInstallTimeout: 应用安装超时时间(毫秒)
# 默认90000ms
options.android_install_timeout = 120000
2.2.2 权限与安全参数
# ========== 权限配置 ==========
# autoGrantPermissions: 自动授予所有权限
# True: 自动授予应用请求的所有权限
# False: 手动处理权限弹窗(默认)
options.auto_grant_permissions = True
# ignoreUnimportantViews: 忽略不重要的视图
# True: 提高元素查找速度,但可能遗漏某些元素
# False: 显示所有元素(默认)
options.ignore_unimportant_views = True
# disableWindowAnimation: 禁用窗口动画
# True: 禁用动画,提高测试稳定性
# False: 保留动画(默认)
options.set_capability('appium:disableWindowAnimation', True)
2.2.3 性能优化参数
# ========== 性能优化配置 ==========
# skipServerInstallation: 跳过UiAutomator2服务安装
# True: 跳过安装(假设已安装),加快启动速度
# False: 每次都检查并安装(默认)
options.skip_server_installation = True
# skipDeviceInitialization: 跳过设备初始化
# True: 跳过设备初始化步骤
# False: 执行完整初始化(默认)
options.skip_device_initialization = True
# skipUnlock: 跳过设备解锁
# True: 假设设备已解锁
# False: 自动尝试解锁(默认)
options.set_capability('appium:skipUnlock', True)
# disableSuppressAccessibilityService: 禁用辅助服务抑制
# True: 允许辅助服务运行
# False: 抑制辅助服务(默认)
options.set_capability('appium:disableSuppressAccessibilityService', True)
2.2.4 ADB相关参数
# ========== ADB配置 ==========
# adbPort: ADB服务端口
# 默认5037
options.set_capability('appium:adbPort', 5037)
# adbExecTimeout: ADB命令执行超时时间(毫秒)
# 默认20000ms
options.adb_exec_timeout = 60000
# androidDeviceReadyTimeout: 设备就绪超时时间(秒)
# 默认30秒
options.set_capability('appium:androidDeviceReadyTimeout', 60)
# avd: 模拟器名称
# 指定要启动的Android模拟器
options.set_capability('appium:avd', 'Pixel_6_API_33')
# avdLaunchTimeout: 模拟器启动超时时间(毫秒)
# 默认120000ms
options.set_capability('appium:avdLaunchTimeout', 180000)
# avdArgs: 模拟器启动参数
options.set_capability('appium:avdArgs', '-gpu host -memory 4096')
2.2.5 安装与多应用参数
# ========== 安装配置 ==========
# enforceAppInstall: 强制安装应用
# True: 即使应用已存在也重新安装
# False: 应用存在则跳过安装(默认)
options.set_capability('appium:enforceAppInstall', False)
# otherApps: 同时安装的其他应用
# JSON数组格式,指定其他需要安装的应用路径
options.set_capability(
'appium:otherApps',
'["/path/to/helper1.apk", "/path/to/helper2.apk"]'
)
# allowTestPackages: 允许安装测试包
# True: 允许安装android:testOnly的应用
# False: 不允许(默认)
options.set_capability('appium:allowTestPackages', True)
# enableMultiWindows: 启用多窗口支持
# True: 支持检测多个窗口
# False: 仅检测当前窗口(默认)
options.set_capability('appium:enableMultiWindows', True)
2.2.6 日志与调试参数
# ========== 日志配置 ==========
# skipLogcatCapture: 跳过logcat日志采集
# True: 不采集logcat日志
# False: 采集logcat日志(默认)
options.set_capability('appium:skipLogcatCapture', False)
# logcatFilterSpecs: logcat过滤规则
# 指定要采集的日志标签
options.set_capability(
'appium:logcatFilterSpecs',
'ActivityManager:I *:S' # 只采集ActivityManager的INFO级别日志
)
2.3 iOS专属Capabilities
2.3.1 应用配置参数
from appium.options.ios import XCUITestOptions
options = XCUITestOptions()
# ========== 应用配置 ==========
# bundleId: 应用Bundle ID(必填,用于已安装应用)
options.bundle_id = 'com.example.app'
# app: 应用安装包路径
# 支持.app和.ipa格式
options.app = '/path/to/app.app'
# updatedWDABundleId: 自定义WDA Bundle ID
# 用于企业签名场景
options.set_capability('appium:updatedWDABundleId', 'com.company.wda')
2.3.2 签名与证书参数
# ========== 签名配置 ==========
# xcodeOrgId: Apple开发者团队ID
options.set_capability('appium:xcodeOrgId', 'TEAM_ID_XXXXXXXX')
# xcodeSigningId: 签名证书类型
# 开发: 'iPhone Developer'
# 发布: 'iPhone Distribution'
options.set_capability('appium:xcodeSigningId', 'iPhone Developer')
# xcodeCertificatePath: 签名证书路径
options.set_capability('appium:xcodeCertificatePath', '/path/to/cert.p12')
# keychainPath: 钥匙串文件路径
options.set_capability('appium:keychainPath', '/path/to/keychain')
# keychainPassword: 钥匙串密码
options.set_capability('appium:keychainPassword', 'password')
2.3.3 WebDriverAgent参数
# ========== WDA配置 ==========
# wdaLocalPort: WDA本地端口
# 用于端口转发,默认8100
options.wda_local_port = 8100
# wdaLaunchTimeout: WDA启动超时时间(毫秒)
# 默认120000ms
options.wda_launch_timeout = 180000
# wdaStartupRetries: WDA启动重试次数
# 默认4次
options.set_capability('appium:wdaStartupRetries', 6)
# wdaStartupRetryInterval: WDA启动重试间隔(毫秒)
# 默认10000ms
options.set_capability('appium:wdaStartupRetryInterval', 15000)
# wdaConnectionTimeout: WDA连接超时时间(毫秒)
# 默认240000ms
options.set_capability('appium:wdaConnectionTimeout', 300000)
# prebuildWDA: 预构建WDA
# True: 使用预构建的WDA,加快启动速度
# False: 每次重新构建(默认)
options.set_capability('appium:prebuildWDA', True)
# usePrebuiltWDA: 使用预构建的WDA
# True: 使用已存在的WDA
# False: 每次重新编译(默认)
options.set_capability('appium:usePrebuiltWDA', True)
2.3.4 性能与行为参数
# ========== 性能配置 ==========
# waitForQuiescence: 等待应用静止
# True: 等待应用完全加载后再开始测试(默认)
# False: 立即开始测试
options.set_capability('appium:waitForQuiescence', False)
# maxTypingFrequency: 最大输入频率
# 控制输入速度,默认60次/秒
options.set_capability('appium:maxTypingFrequency', 30)
# simpleIsVisibleCheck: 简单可见性检查
# True: 使用简化的可见性检查算法
# False: 使用完整的可见性检查(默认)
options.set_capability('appium:simpleIsVisibleCheck', True)
# shouldUseSingletonTestManager: 使用单例测试管理器
# True: 使用单例模式,提高性能
# False: 每次创建新实例(默认)
options.set_capability('appium:shouldUseSingletonTestManager', True)
2.3.5 弹窗与系统参数
# ========== 弹窗处理配置 ==========
# autoAcceptAlerts: 自动接受系统弹窗
# True: 自动点击"允许"按钮
# False: 手动处理弹窗(默认)
options.set_capability('appium:autoAcceptAlerts', True)
# autoDismissAlerts: 自动拒绝系统弹窗
# True: 自动点击"不允许"按钮
# False: 手动处理弹窗(默认)
options.set_capability('appium:autoDismissAlerts', False)
# ========== 数据清理配置 ==========
# clearSystemFiles: 清理系统临时文件
# True: 测试结束后清理
# False: 保留临时文件(默认)
options.set_capability('appium:clearSystemFiles', True)
# keepKeyChains: 保留钥匙串数据
# True: 保留钥匙串数据
# False: 清理钥匙串数据(默认)
options.set_capability('appium:keepKeyChains', True)
2.4 设备解锁参数
# ========== 设备解锁配置 ==========
# skipUnlock: 跳过解锁检查
# True: 假设设备已解锁
# False: 检查并尝试解锁(默认)
options.set_capability('appium:skipUnlock', False)
# unlockType: 解锁类型
# 'pin': PIN码解锁
# 'password': 密码解锁
# 'pattern': 图案解锁
# 'fingerprint': 指纹解锁
options.set_capability('appium:unlockType', 'pin')
# unlockKey: 解锁密码/图案
# pin/password: 直接输入密码,如'1234'
# pattern: 图案点序列,如'123456789'(九宫格从1到9)
options.set_capability('appium:unlockKey', '1234')
# unlockSuccessTimeout: 解锁成功等待时间(毫秒)
# 默认2000ms
options.set_capability('appium:unlockSuccessTimeout', 3000)
解锁场景示例:
from appium import webdriver
from appium.options.android import UiAutomator2Options
# PIN码解锁配置
options = UiAutomator2Options()
options.platform_name = 'Android'
options.device_name = 'emulator-5554'
options.automation_name = 'UiAutomator2'
options.set_capability('appium:unlockType', 'pin')
options.set_capability('appium:unlockKey', '1234')
driver = webdriver.Remote('http://127.0.0.1:4723', options=options)
# 图案解锁配置(L形图案)
options.set_capability('appium:unlockType', 'pattern')
options.set_capability('appium:unlockKey', '14789') # L形图案
2.5 WebView参数
# ========== WebView配置 ==========
# autoWebview: 自动切换到WebView上下文
# True: 自动切换到第一个WebView
# False: 保持原生上下文(默认)
options.set_capability('appium:autoWebview', True)
# autoWebviewName: 指定要切换的WebView名称
options.set_capability('appium:autoWebviewName', 'WEBVIEW_com.example.app')
# webviewConnectTimeout: WebView连接超时时间(毫秒)
# 默认5000ms
options.set_capability('appium:webviewConnectTimeout', 10000)
# webviewConnectRetries: WebView连接重试次数
# 默认8次
options.set_capability('appium:webviewConnectRetries', 10)
# enableWebviewDetailsCollection: 启用WebView详情收集
# True: 收集更详细的WebView信息
# False: 基本信息收集(默认)
options.set_capability('appium:enableWebviewDetailsCollection', True)
2.6 视频录制参数
# ========== 视频录制配置 ==========
# videoQuality: 视频质量
# Android: 0(低), 1(中), 2(高)
# iOS: 'low', 'medium', 'high', 'photo'
options.set_capability('appium:videoQuality', 'medium')
# videoType: 视频格式
# Android: 'mp4'(默认), 'h264'
# iOS: 'h264', 'mp4'
options.set_capability('appium:videoType', 'mp4')
# videoFps: 视频帧率
# 默认10fps
options.set_capability('appium:videoFps', 15)
# videoScale: 视频缩放比例
# 格式: 'width:height'
options.set_capability('appium:videoScale', '1280:720')
# videoSize: 视频尺寸
# 格式: 'widthxheight'
options.set_capability('appium:videoSize', '1280x720')
# timeLimit: 录制时长限制(秒)
# Android默认180秒,iOS默认180秒
options.set_capability('appium:timeLimit', 300)
2.7 调试与性能参数
# ========== 调试配置 ==========
# printPageSourceOnFindFailure: 元素查找失败时打印页面源码
# True: 查找失败时自动打印
# False: 不打印(默认)
options.set_capability('appium:printPageSourceOnFindFailure', True)
# eventTimings: 记录事件时间
# True: 记录每个操作的时间戳
# False: 不记录(默认)
options.set_capability('appium:eventTimings', True)
# enablePerformanceLogging: 启用性能日志
# True: 收集性能数据
# False: 不收集(默认)
options.set_capability('appium:enablePerformanceLogging', True)
# relaxedSecurityEnabled: 启用宽松安全模式
# True: 允许执行更多高级命令
# False: 标准安全模式(默认)
options.set_capability('appium:relaxedSecurityEnabled', True)
# mjpegServerPort: MJPEG服务端口
# 用于实时查看设备屏幕
options.set_capability('appium:mjpegServerPort', 9200)
# ========== 日志配置 ==========
# skipLogCapture: 跳过日志采集
# True: 不采集设备日志
# False: 采集日志(默认)
options.set_capability('appium:skipLogCapture', False)
# showIosLog: 显示iOS日志
# True: 显示WDA日志
# False: 不显示(默认)
options.set_capability('appium:showIosLog', True)
2.8 W3C WebDriver协议Capabilities规范
2.8.1 W3C标准Capabilities
# W3C标准Capabilities格式
from appium.options.common.base import AppiumOptions
options = AppiumOptions()
# W3C标准定义的Capabilities
options.set_capability('browserName', '') # 空字符串表示原生应用
options.set_capability('platformName', 'Android')
options.set_capability('acceptInsecureCerts', True)
options.set_capability('pageLoadStrategy', 'normal')
options.set_capability('setWindowRect', True)
options.set_capability('strictFileInteractability', False)
options.set_capability('unhandledPromptBehavior', 'dismiss')
# Appium扩展Capabilities需要使用appium:前缀
options.set_capability('appium:deviceName', 'emulator-5554')
options.set_capability('appium:automationName', 'UiAutomator2')
options.set_capability('appium:appPackage', 'com.example.app')
2.8.2 JSON格式示例
{
"capabilities": {
"alwaysMatch": {
"platformName": "Android",
"appium:deviceName": "emulator-5554",
"appium:automationName": "UiAutomator2",
"appium:appPackage": "com.example.app",
"appium:appActivity": ".MainActivity",
"appium:noReset": false,
"appium:newCommandTimeout": 300
},
"firstMatch": [
{
"appium:udid": "emulator-5554"
}
]
}
}
2.9 Capabilities最佳实践配置模板
2.9.1 Android配置模板
from appium import webdriver
from appium.options.android import UiAutomator2Options
def create_android_driver(device_udid=None, app_path=None):
"""创建Android Driver的标准配置"""
options = UiAutomator2Options()
# 平台配置
options.platform_name = 'Android'
options.automation_name = 'UiAutomator2'
# 设备配置
if device_udid:
options.udid = device_udid
# 应用配置
if app_path:
options.app = app_path
else:
options.app_package = 'com.example.app'
options.app_activity = '.MainActivity'
# 性能优化配置
options.no_reset = False
options.new_command_timeout = 300
options.skip_server_installation = False
options.adb_exec_timeout = 60000
# 权限配置
options.auto_grant_permissions = True
# 调试配置
options.set_capability('appium:printPageSourceOnFindFailure', True)
options.set_capability('appium:disableWindowAnimation', True)
# 创建Driver
driver = webdriver.Remote(
'http://127.0.0.1:4723',
options=options
)
return driver
2.9.2 iOS配置模板
from appium import webdriver
from appium.options.ios import XCUITestOptions
def create_ios_driver(device_udid=None, app_path=None):
"""创建iOS Driver的标准配置"""
options = XCUITestOptions()
# 平台配置
options.platform_name = 'iOS'
options.automation_name = 'XCUITest'
# 设备配置
if device_udid:
options.udid = device_udid
# 应用配置
if app_path:
options.app = app_path
else:
options.bundle_id = 'com.example.app'
# WDA配置
options.wda_local_port = 8100
options.wda_launch_timeout = 180000
options.set_capability('appium:wdaStartupRetries', 4)
# 性能配置
options.set_capability('appium:maxTypingFrequency', 30)
options.set_capability('appium:waitForQuiescence', False)
# 创建Driver
driver = webdriver.Remote(
'http://127.0.0.1:4723',
options=options
)
return driver
第三章:元素定位策略
3.1 元素检查工具
3.1.1 Appium Inspector
Appium Inspector是最常用的元素检查工具,支持Android和iOS平台。
Appium Inspector主要功能:
| 功能 | 说明 | 使用场景 |
|---|---|---|
| 元素树展示 | 显示页面元素的层级结构 | 理解元素嵌套关系 |
| 属性面板 | 显示选中元素的所有属性 | 获取定位所需属性 |
| 定位器生成 | 自动生成多种定位器 | 快速获取定位代码 |
| 交互测试 | 直接点击/输入元素 | 验证定位正确性 |
| 截图标注 | 在截图上标注元素位置 | 文档和报告 |
Appium Inspector配置示例:
{
"platformName": "Android",
"appium:deviceName": "emulator-5554",
"appium:automationName": "UiAutomator2",
"appium:appPackage": "com.example.app",
"appium:appActivity": ".MainActivity",
"appium:noReset": true
}
3.1.2 UIAutomatorViewer
UIAutomatorViewer是Android SDK自带的元素检查工具:
# 启动UIAutomatorViewer
# 位置: $ANDROID_HOME/tools/bin/uiautomatorviewer
cd $ANDROID_HOME/tools/bin
./uiautomatorviewer
使用步骤:
- 点击"Device Screenshot"按钮获取当前屏幕截图
- 等待页面加载完成
- 在左侧截图上点击目标元素
- 在右侧面板查看元素属性
3.1.3 Xcode Accessibility Inspector
iOS开发者可以使用Xcode内置的Accessibility Inspector:
# 打开方式
# Xcode -> Open Developer Tool -> Accessibility Inspector
3.2 原生定位方式
3.2.1 ID定位
ID定位是最推荐的方式,速度最快、最稳定:
from appium import webdriver
from appium.webdriver.common.appiumby import AppiumBy
# ID定位示例
element = driver.find_element(AppiumBy.ID, 'com.example.app:id/login_button')
element.click()
# ID定位说明
# Android: resource-id属性,格式为"包名:id/元素名"
# iOS: name属性或accessibilityIdentifier
ID定位最佳实践:
class LoginPageLocators:
"""登录页面元素定位器类"""
# 使用常量定义定位器,便于维护
USERNAME_INPUT = (AppiumBy.ID, 'com.example.app:id/username')
PASSWORD_INPUT = (AppiumBy.ID, 'com.example.app:id/password')
LOGIN_BUTTON = (AppiumBy.ID, 'com.example.app:id/login_button')
ERROR_MESSAGE = (AppiumBy.ID, 'com.example.app:id/error_text')
# 使用示例
def login(driver, username, password):
"""登录操作封装"""
# 输入用户名
username_input = driver.find_element(*LoginPageLocators.USERNAME_INPUT)
username_input.clear()
username_input.send_keys(username)
# 输入密码
password_input = driver.find_element(*LoginPageLocators.PASSWORD_INPUT)
password_input.clear()
password_input.send_keys(password)
# 点击登录
driver.find_element(*LoginPageLocators.LOGIN_BUTTON).click()
3.2.2 ClassName定位
ClassName定位通过元素的类名进行定位:
# ClassName定位示例
# Android: 元素的class属性
# iOS: 元素的type属性
# 查找所有EditText元素(Android)
edit_texts = driver.find_elements(AppiumBy.CLASS_NAME, 'android.widget.EditText')
# 查找所有Button元素
buttons = driver.find_elements(AppiumBy.CLASS_NAME, 'android.widget.Button')
# iOS示例 - 查找所有文本框
text_fields = driver.find_elements(AppiumBy.CLASS_NAME, 'XCUIElementTypeTextField')
注意: ClassName定位通常返回多个元素,需要结合其他条件筛选:
# ClassName定位结合索引使用
edit_texts = driver.find_elements(AppiumBy.CLASS_NAME, 'android.widget.EditText')
if len(edit_texts) >= 2:
# 获取第二个输入框
second_input = edit_texts[1]
second_input.send_keys('test')
3.2.3 AccessibilityId定位
AccessibilityId定位是跨平台的推荐方式:
# AccessibilityId定位示例
# Android: content-desc属性
# iOS: accessibilityIdentifier属性
# 使用AccessibilityId定位
element = driver.find_element(AppiumBy.ACCESSIBILITY_ID, 'login_button')
element.click()
# AccessibilityId定位的优势:
# 1. 跨平台统一(Android和iOS使用相同代码)
# 2. 开发人员可以自定义,便于维护
# 3. 对用户可见性无要求
开发端配置AccessibilityId:
// Android (Kotlin)
button.contentDescription = "login_button"
// 或在XML中设置
// android:contentDescription="login_button"
// iOS (Swift)
button.accessibilityIdentifier = "login_button"
3.2.4 XPath定位
XPath定位功能强大,但性能较差,应谨慎使用:
# XPath定位示例
# 基本语法
element = driver.find_element(AppiumBy.XPATH, '//android.widget.Button[@text="登录"]')
# 常用XPath表达式
# 1. 通过text属性定位
login_btn = driver.find_element(AppiumBy.XPATH, '//*[@text="登录"]')
# 2. 通过resource-id定位
element = driver.find_element(
AppiumBy.XPATH,
'//*[@resource-id="com.example.app:id/login_button"]'
)
# 3. 通过class和属性组合定位
element = driver.find_element(
AppiumBy.XPATH,
'//android.widget.EditText[@resource-id="com.example.app:id/username" and @text="请输入用户名"]'
)
# 4. 通过索引定位(注意:索引从1开始)
element = driver.find_element(AppiumBy.XPATH, '//android.widget.EditText[1]')
# 5. 通过父子关系定位
element = driver.find_element(
AppiumBy.XPATH,
'//android.widget.LinearLayout/android.widget.Button[1]'
)
# 6. 通过包含文本定位
element = driver.find_element(AppiumBy.XPATH, '//*[contains(@text, "登录")]')
# 7. 通过轴定位
element = driver.find_element(
AppiumBy.XPATH,
'//android.widget.TextView[@text="用户名"]/following-sibling::android.widget.EditText'
)
XPath优化技巧:
# 不推荐:使用通配符和长路径
bad_xpath = '//*[@class="android.widget.LinearLayout"]/*/*[@text="登录"]'
# 推荐:使用具体路径和属性
good_xpath = '//android.widget.Button[@resource-id="com.example.app:id/login_button"]'
# 不推荐:使用text属性(多语言环境下不稳定)
bad_xpath = '//*[@text="Login"]'
# 推荐:使用resource-id或content-desc
good_xpath = '//*[@resource-id="com.example.app:id/login_button"]'
3.3 Android UIAutomator定位器
UIAutomator定位器是Android平台特有的强大定位方式:
# UIAutomator定位器使用UiSelector和UiScrollable
# 1. 基本UiSelector定位
element = driver.find_element(
AppiumBy.ANDROID_UIAUTOMATOR,
'new UiSelector().text("登录")'
)
# 2. 通过resource-id定位
element = driver.find_element(
AppiumBy.ANDROID_UIAUTOMATOR,
'new UiSelector().resourceId("com.example.app:id/login_button")'
)
# 3. 通过className定位
element = driver.find_element(
AppiumBy.ANDROID_UIAUTOMATOR,
'new UiSelector().className("android.widget.Button")'
)
# 4. 通过description定位
element = driver.find_element(
AppiumBy.ANDROID_UIAUTOMATOR,
'new UiSelector().description("登录按钮")'
)
# 5. 组合条件定位
element = driver.find_element(
AppiumBy.ANDROID_UIAUTOMATOR,
'new UiSelector().className("android.widget.Button").text("登录")'
)
# 6. 索引定位
element = driver.find_element(
AppiumBy.ANDROID_UIAUTOMATOR,
'new UiSelector().className("android.widget.EditText").index(0)'
)
# 7. 包含文本定位
element = driver.find_element(
AppiumBy.ANDROID_UIAUTOMATOR,
'new UiSelector().textContains("用户")'
)
# 8. 正则表达式匹配
element = driver.find_element(
AppiumBy.ANDROID_UIAUTOMATOR,
'new UiSelector().textMatches(".*登录.*")'
)
UiScrollable滚动定位:
# 滚动查找元素 - 垂直滚动
element = driver.find_element(
AppiumBy.ANDROID_UIAUTOMATOR,
'''
new UiScrollable(new UiSelector().scrollable(true))
.scrollIntoView(new UiSelector().text("目标元素文本"))
'''
)
# 滚动查找元素 - 水平滚动
element = driver.find_element(
AppiumBy.ANDROID_UIAUTOMATOR,
'''
new UiScrollable(new UiSelector().scrollable(true))
.setAsHorizontalList()
.scrollIntoView(new UiSelector().text("目标元素文本"))
'''
)
# 设置最大滚动次数
element = driver.find_element(
AppiumBy.ANDROID_UIAUTOMATOR,
'''
new UiScrollable(new UiSelector().scrollable(true))
.setMaxSearchSwipes(10)
.scrollIntoView(new UiSelector().resourceId("com.example.app:id/target"))
'''
)
3.4 iOS Predicate String与Class Chain定位
3.4.1 Predicate String定位
Predicate String是iOS平台的高效定位方式:
# Predicate String定位示例
# 1. 通过name属性定位
element = driver.find_element(
AppiumBy.IOS_PREDICATE,
'name == "登录按钮"'
)
# 2. 通过label属性定位
element = driver.find_element(
AppiumBy.IOS_PREDICATE,
'label == "用户名"'
)
# 3. 通过type属性定位
element = driver.find_element(
AppiumBy.IOS_PREDICATE,
'type == "XCUIElementTypeButton"'
)
# 4. 组合条件定位
element = driver.find_element(
AppiumBy.IOS_PREDICATE,
'type == "XCUIElementTypeButton" AND name == "登录"'
)
# 5. 包含匹配
element = driver.find_element(
AppiumBy.IOS_PREDICATE,
'name CONTAINS "登录"'
)
# 6. 以...开头匹配
element = driver.find_element(
AppiumBy.IOS_PREDICATE,
'name BEGINSWITH "用户"'
)
# 7. 正则表达式匹配
element = driver.find_element(
AppiumBy.IOS_PREDICATE,
'name MATCHES ".*登录.*"'
)
# 8. 多条件OR定位
element = driver.find_element(
AppiumBy.IOS_PREDICATE,
'name == "登录" OR name == "Login"'
)
# 9. 通过value属性定位
element = driver.find_element(
AppiumBy.IOS_PREDICATE,
'value == "请输入用户名"'
)
# 10. 可见性筛选
element = driver.find_element(
AppiumBy.IOS_PREDICATE,
'type == "XCUIElementTypeButton" AND visible == 1'
)
3.4.2 Class Chain定位
Class Chain定位是iOS特有的链式定位方式:
# Class Chain定位示例
# 1. 基本类型定位
element = driver.find_element(
AppiumBy.IOS_CLASS_CHAIN,
'**/XCUIElementTypeButton'
)
# 2. 通过属性定位
element = driver.find_element(
AppiumBy.IOS_CLASS_CHAIN,
'**/XCUIElementTypeButton[`name == "登录"`]'
)
# 3. 层级路径定位
element = driver.find_element(
AppiumBy.IOS_CLASS_CHAIN,
'**/XCUIElementTypeWindow/XCUIElementTypeOther/XCUIElementTypeButton'
)
# 4. 索引定位(索引从0开始)
element = driver.find_element(
AppiumBy.IOS_CLASS_CHAIN,
'**/XCUIElementTypeButton[0]'
)
# 5. 后代元素定位
element = driver.find_element(
AppiumBy.IOS_CLASS_CHAIN,
'**/XCUIElementTypeTable/**/XCUIElementTypeCell'
)
# 6. 组合条件
element = driver.find_element(
AppiumBy.IOS_CLASS_CHAIN,
'**/XCUIElementTypeButton[`name == "登录" AND visible == 1`]'
)
3.5 图像识别定位
Appium 2.0通过Images插件支持图像识别定位:
3.5.1 安装Images插件
# 安装images插件
appium plugin install images
# 启动Appium时加载插件
appium --use-plugins=images
3.5.2 图像定位使用
import base64
from appium import webdriver
from appium.webdriver.common.appiumby import AppiumBy
# 读取基准图片
with open('login_button.png', 'rb') as f:
image_data = base64.b64encode(f.read()).decode('utf-8')
# 方法1:通过图片文件路径定位
element = driver.find_element(
AppiumBy.IMAGE,
'/path/to/login_button.png'
)
# 方法2:通过Base64编码定位
element = driver.find_element(
AppiumBy.IMAGE,
image_data
)
# 设置图像匹配阈值(0-1之间,默认0.5)
driver.update_settings({
'imageMatchThreshold': 0.8,
'imageElementTapStrategy': 'tapElementCenter'
})
# 查找所有匹配的图像元素
elements = driver.find_elements(AppiumBy.IMAGE, image_data)
print(f"找到 {len(elements)} 个匹配元素")
图像定位最佳实践:
class ImageLocator:
"""图像定位封装类"""
def __init__(self, driver, image_dir='images'):
self.driver = driver
self.image_dir = image_dir
# 设置图像识别参数
self.driver.update_settings({
'imageMatchThreshold': 0.7, # 匹配阈值
'fixImageFindScreenshotDims': True,
'fixImageTemplateSize': True,
'autoUpdateImageElementPosition': True
})
def find_by_image(self, image_name, threshold=0.7):
"""通过图片名称查找元素"""
image_path = f'{self.image_dir}/{image_name}'
# 临时调整阈值
self.driver.update_settings({'imageMatchThreshold': threshold})
try:
element = self.driver.find_element(AppiumBy.IMAGE, image_path)
return element
except Exception as e:
print(f"图像定位失败: {image_name}, 错误: {e}")
return None
def click_image(self, image_name, threshold=0.7):
"""点击图片元素"""
element = self.find_by_image(image_name, threshold)
if element:
element.click()
return True
return False
# 使用示例
image_locator = ImageLocator(driver, 'test_images')
image_locator.click_image('login_button.png', threshold=0.8)
3.6 元素定位策略选择与性能对比
3.6.1 定位策略性能对比
性能测试代码:
import time
from appium.webdriver.common.appiumby import AppiumBy
def benchmark_locators(driver, iterations=100):
"""定位策略性能基准测试"""
locators = {
'ID': (AppiumBy.ID, 'com.example.app:id/login_button'),
'AccessibilityId': (AppiumBy.ACCESSIBILITY_ID, 'login_button'),
'ClassName': (AppiumBy.CLASS_NAME, 'android.widget.Button'),
'XPath': (AppiumBy.XPATH, '//*[@resource-id="com.example.app:id/login_button"]'),
'UIAutomator': (AppiumBy.ANDROID_UIAUTOMATOR,
'new UiSelector().resourceId("com.example.app:id/login_button")')
}
results = {}
for name, (by, value) in locators.items():
start_time = time.time()
for _ in range(iterations):
try:
driver.find_element(by, value)
except:
pass
elapsed = time.time() - start_time
avg_time = elapsed / iterations
results[name] = avg_time
print(f"{name}: 平均耗时 {avg_time:.4f}秒")
return results
3.6.2 定位策略选择原则
| 优先级 | 定位策略 | 适用场景 | 注意事项 |
|---|---|---|---|
| 1 | ID | 元素有唯一resource-id | 最推荐,速度最快 |
| 2 | AccessibilityId | 开发配置了accessibility标识 | 跨平台兼容 |
| 3 | Android UIAutomator | 需要复杂条件或滚动查找 | Android专用 |
| 4 | iOS Predicate | iOS平台高效定位 | iOS专用 |
| 5 | ClassName | 同类元素批量操作 | 通常返回多个 |
| 6 | XPath | 其他方式无法定位时使用 | 性能较差,避免深层嵌套 |
| 7 | 图像识别 | 无可用属性时使用 | 需要维护图片资源 |
3.7 元素定位失败原因分析与解决方案
3.7.1 常见失败原因
3.7.2 问题排查代码
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from appium.webdriver.common.appiumby import AppiumBy
class ElementLocator:
"""元素定位封装类,包含错误诊断"""
def __init__(self, driver, timeout=10):
self.driver = driver
self.timeout = timeout
def find_element_safe(self, by, value, diagnose=True):
"""安全定位元素,失败时提供诊断信息"""
try:
element = WebDriverWait(self.driver, self.timeout).until(
EC.presence_of_element_located((by, value))
)
return element
except Exception as e:
if diagnose:
self._diagnose_failure(by, value)
raise
def _diagnose_failure(self, by, value):
"""诊断定位失败原因"""
print("\n========== 元素定位失败诊断 ==========")
print(f"定位方式: {by}")
print(f"定位值: {value}")
# 1. 检查页面源码
page_source = self.driver.page_source
print(f"\n页面源码长度: {len(page_source)}")
# 2. 检查是否在正确的上下文
current_context = self.driver.current_context
print(f"当前上下文: {current_context}")
# 3. 检查当前Activity(Android)
try:
current_activity = self.driver.current_activity
print(f"当前Activity: {current_activity}")
except:
pass
# 4. 保存截图
screenshot_path = '/tmp/locator_failure.png'
self.driver.save_screenshot(screenshot_path)
print(f"截图已保存: {screenshot_path}")
# 5. 检查是否有相似元素
if by == AppiumBy.ID:
similar = self.driver.find_elements(AppiumBy.XPATH,
f'//*[contains(@resource-id, "{value.split("/")[-1]}")]')
print(f"相似ID元素数量: {len(similar)}")
print("========================================\n")
def find_with_retry(self, by, value, max_retries=3, retry_interval=2):
"""带重试的元素定位"""
import time
for attempt in range(max_retries):
try:
element = self.driver.find_element(by, value)
if element.is_displayed():
return element
except:
if attempt < max_retries - 1:
print(f"第{attempt + 1}次定位失败,等待重试...")
time.sleep(retry_interval)
raise Exception(f"元素定位失败,已重试{max_retries}次")
# 使用示例
locator = ElementLocator(driver)
element = locator.find_element_safe(AppiumBy.ID, 'com.example.app:id/login_button')
3.8 多分辨率适配下的元素定位策略
3.8.1 响应式定位策略
class ResponsiveLocator:
"""多分辨率适配定位类"""
def __init__(self, driver):
self.driver = driver
self.screen_size = driver.get_window_size()
self.width = self.screen_size['width']
self.height = self.screen_size['height']
def get_element_by_ratio(self, x_ratio, y_ratio):
"""通过屏幕比例定位元素"""
x = int(self.width * x_ratio)
y = int(self.height * y_ratio)
# 使用坐标点击
self.driver.tap([(x, y)])
def is_small_screen(self):
"""判断是否为小屏幕"""
return self.width < 720
def is_large_screen(self):
"""判断是否为大屏幕"""
return self.width >= 1080
def find_element_adaptive(self, locators):
"""根据屏幕尺寸选择定位器"""
if self.is_small_screen() and 'small' in locators:
return self.driver.find_element(*locators['small'])
elif self.is_large_screen() and 'large' in locators:
return self.driver.find_element(*locators['large'])
else:
return self.driver.find_element(*locators['default'])
# 使用示例
locators = {
'small': (AppiumBy.ID, 'com.example.app:id/button_small'),
'default': (AppiumBy.ID, 'com.example.app:id/button'),
'large': (AppiumBy.ID, 'com.example.app:id/button_large')
}
responsive = ResponsiveLocator(driver)
element = responsive.find_element_adaptive(locators)
3.8.2 相对坐标定位
class RelativeCoordinateLocator:
"""相对坐标定位类"""
def __init__(self, driver):
self.driver = driver
self.size = driver.get_window_size()
def tap_by_ratio(self, x_ratio, y_ratio):
"""按比例点击屏幕位置"""
x = int(self.size['width'] * x_ratio)
y = int(self.size['height'] * y_ratio)
self.driver.tap([(x, y)])
def tap_element_center(self, element):
"""点击元素中心"""
rect = element.rect
center_x = rect['x'] + rect['width'] // 2
center_y = rect['y'] + rect['height'] // 2
self.driver.tap([(center_x, center_y)])
def swipe_by_ratio(self, start_ratio, end_ratio, duration=500):
"""按比例滑动"""
start_x = int(self.size['width'] * start_ratio[0])
start_y = int(self.size['height'] * start_ratio[1])
end_x = int(self.size['width'] * end_ratio[0])
end_y = int(self.size['height'] * end_ratio[1])
self.driver.swipe(start_x, start_y, end_x, end_y, duration)
# 使用示例
coord_locator = RelativeCoordinateLocator(driver)
# 点击屏幕中心
coord_locator.tap_by_ratio(0.5, 0.5)
# 从屏幕底部滑到顶部(下拉刷新)
coord_locator.swipe_by_ratio((0.5, 0.8), (0.5, 0.2))
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)