第一章:Appium基础入门

1.1 Appium概述与架构原理

1.1.1 什么是Appium

Appium是一个开源的、跨平台的移动应用自动化测试框架,它允许开发者使用相同的API对iOS、Android和Windows平台上的原生应用、移动Web应用和混合应用进行自动化测试。Appium的设计理念是"无需重新编译应用即可测试",这使得它成为移动自动化测试领域的首选工具。

1.1.2 C/S架构详解

Appium采用经典的客户端/服务器(Client/Server)架构,这种设计使得测试代码与被测应用完全解耦。

设备层

驱动层

服务端层

客户端层

Python客户端

Java客户端

JavaScript客户端

其他语言客户端

Appium Server

Driver Manager

UiAutomator2 Driver

XCUITest Driver

Espresso Driver

Android设备/模拟器

iOS设备/模拟器

架构组件说明:

组件 职责 说明
客户端 发送WebDriver命令 支持多种编程语言,提供统一的API
Appium Server 接收并处理请求 Node.js实现的HTTP服务器,监听客户端请求
Driver Manager 管理驱动程序 Appium 2.0新增组件,负责驱动的安装和管理
Driver 执行平台特定命令 将WebDriver命令转换为平台特定的自动化指令
设备层 运行被测应用 真实设备或模拟器
1.1.3 Session机制

Session是Appium的核心概念,每个自动化测试会话都通过Session来管理:

设备 Driver Appium Server 测试客户端 设备 Driver Appium Server 测试客户端 loop [测试执行过程] POST /session (创建会话请求) 初始化Driver 连接设备 连接成功 返回Session ID 返回Session信息 执行命令 (携带Session ID) 转发命令 执行操作 返回结果 返回结果 返回响应 DELETE /session/:id (删除会话) 清理资源 断开连接 会话已关闭

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独立出来,实现了按需安装:

Appium 2.0

按需安装

按需安装

按需安装

Appium Server

Driver Manager

UiAutomator2

XCUITest

插件系统

Appium 1.x

Appium Server

内置所有Driver

UiAutomator2

XCUITest

其他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框架:

UiAutomator2架构

Appium Client

Appium Server

UiAutomator2 Driver

io.appium.uiautomator2.server

io.appium.uiautomator2.server.test

Android Accessibility Service

目标应用

工作原理:

  1. Driver将两个APK安装到设备上(server和server.test)
  2. 通过Android Accessibility Service获取元素信息
  3. 使用UiAutomator2 API执行操作

适用场景:

  • Android 5.0及以上版本
  • 原生应用测试
  • 混合应用测试
  • 需要跨应用操作的场景
1.3.2 XCUITest Driver

XCUITest是iOS平台的主要驱动,基于Apple的XCUITest框架:

XCUITest架构

Appium Client

Appium Server

XCUITest Driver

WebDriverAgent

XCUITest Framework

iOS Accessibility

目标应用

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工作原理
目标应用 Android设备 UiAutomator2 Driver Appium Server 测试脚本 目标应用 Android设备 UiAutomator2 Driver Appium Server 测试脚本 发送操作命令 解析命令 通过ADB发送指令 执行Accessibility操作 返回结果 返回响应 封装响应 返回结果

核心组件:

  • UiAutomator2 Server APK:运行在设备上的服务端
  • Accessibility Service:获取屏幕元素信息
  • ADB Bridge:与设备通信的桥梁
1.4.2 XCUITest工作原理
目标应用 iOS设备 WebDriverAgent XCUITest Driver Appium Server 测试脚本 目标应用 iOS设备 WebDriverAgent XCUITest Driver Appium Server 测试脚本 发送操作命令 解析命令 发送HTTP请求 执行XCUITest命令 执行Accessibility操作 返回结果 返回响应 返回JSON响应 封装响应 返回结果

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 Desktop功能

Server启动界面

配置管理

日志查看

Inspector界面

元素定位

操作录制

树形结构展示

主要功能:

  • 一键启动/停止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端点概览

Appium REST API

POST /session

创建新会话

DELETE /session/:id

删除会话

GET /status

获取服务状态

POST /session/:id/element

查找元素

POST /session/:id/element/:id/click

点击元素

POST /session/:id/element/:id/value

输入文本

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 并发限制说明

多Server解决方案

Appium Server 1

Session 1

Appium Server 2

Session 2

Appium Server 3

Session 3

单Server限制

只能处理一个Session

等待

等待

Appium Server

Session 1

Session 2

Session 3

核心限制:

  • 一个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 脚本执行流程图

开始

配置Capabilities

创建WebDriver实例

连接Appium Server

连接成功?

启动应用

报错退出

执行测试用例

查找元素

执行操作

验证结果

测试通过?

记录成功

记录失败并截图

清理资源

关闭Driver

结束


第二章: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

配置Capabilities

连接设备

加载页面元素

选择元素

查看属性

生成定位器

复制到脚本

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

使用步骤:

  1. 点击"Device Screenshot"按钮获取当前屏幕截图
  2. 等待页面加载完成
  3. 在左侧截图上点击目标元素
  4. 在右侧面板查看元素属性
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 定位策略性能对比

性能排序(从快到慢)

ID定位

AccessibilityId

ClassName

Android UIAutomator

iOS Predicate

iOS Class Chain

XPath

图像识别

性能测试代码:

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 常见失败原因

元素定位失败

失败原因分析

页面未完全加载

元素在iframe中

元素被遮挡

元素动态变化

定位器错误

应用包名/Activity错误

增加等待时间

切换到iframe上下文

滚动到可见区域

使用更稳定的定位器

检查并修正定位器

验证应用配置

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))

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐