掌握跨平台自动化测试框架Appium与Selenium实战技巧解决兼容性难题与效率提升

引言:自动化测试在现代软件开发中的重要性
在移动应用和Web应用快速迭代的今天,自动化测试已经成为保障软件质量、提升开发效率的关键环节。Appium和Selenium作为业界领先的自动化测试框架,分别专注于移动端和Web端的测试,而它们的结合使用能够为跨平台应用提供全面的测试覆盖。本文将深入探讨如何掌握这两个框架的实战技巧,解决兼容性难题,并显著提升测试效率。
一、Appium与Selenium基础架构解析
1.1 Appium的核心架构与工作原理
Appium是一个开源的自动化测试框架,支持iOS、Android和Windows原生应用以及混合应用和移动Web应用的测试。其核心优势在于”一次编写,多平台运行”的理念。
Appium的核心组件:
Appium Server
:基于Node.js构建的HTTP服务器,接收客户端请求并转发给对应的设备
Appium Clients
:支持多种语言的客户端库(Java、Python、Ruby、JavaScript等)
WebDriver协议
:基于W3C WebDriver标准,与Selenium保持兼容
设备自动化引擎
:
iOS:使用XCUITest(iOS 10+)或UIAutomation(iOS 9及以下)
Android:使用UiAutomator2(Android 5.0+)或Espresso(可选)
Windows:使用WinAppDriver
工作流程示例:
客户端代码 → Appium Server → 设备自动化引擎 → 目标应用
1.2 Selenium WebDriver架构
Selenium是Web自动化测试的事实标准,其架构设计为Appium提供了理论基础和协议兼容。
Selenium 4的核心组件:
WebDriver
:浏览器自动化引擎
Grid
:分布式测试执行环境
Selenium Server
:独立运行或Grid模式下的中心节点
客户端库
:多种语言绑定
Selenium 4的新特性:
完全支持W3C WebDriver标准
相对定位器(Relative Locators)
Chrome DevTools Protocol集成
改进的Grid架构
二、环境搭建与配置最佳实践
2.1 Appium环境配置(以Android为例)
前提条件:
Node.js(14.x以上)
JDK 8+
Android SDK(包含adb、emulator等工具)
Python/Java等编程语言环境
详细安装步骤:
# 1. 安装Appium Server
npm install -g appium@next # 安装最新版本
# 或者使用Appium Desktop(带GUI界面)
# 下载地址:https://github.com/appium/appium-desktop/releases

# 2. 安装Appium Python客户端
pip install Appium-Python-Client

# 3. 配置Android环境变量
# 在~/.bashrc或~/.zshrc中添加:
export ANDROID_HOME=/path/to/android-sdk
export PATH=$PATH:$ANDROID_HOME/tools
export PATH=$PATH:$ANDROID_HOME/platform-tools

# 4. 验证环境
adb devices # 应该能看到连接的设备或模拟器
appium –version # 显示Appium版本
启动Appium Server:
# 基础启动
appium

# 带详细日志的启动(调试用)
appium –log-level debug –log-timestamp –local-timezone

# 指定端口和日志文件
appium –port 4723 –log /path/to/appium.log
2.2 Selenium环境配置
Python环境配置:
# 安装Selenium 4
pip install selenium>=4.0.0

# 下载浏览器驱动(以Chrome为例)
# 下载地址:https://chromedriver.chromium.org/downloads
# 需要与浏览器版本匹配

# 验证安装
python -c “from selenium import webdriver; print(‘Selenium installed successfully’)”
Java环境配置(Maven):

org.seleniumhq.selenium
selenium-java
4.14.0

2.3 统一配置管理
为了提升效率,建议使用配置文件管理不同环境的参数:
config.py
import os
from dataclasses import dataclass

@dataclass
class AppConfig:
# Appium配置
APPIUM_SERVER_URL = “http://localhost:4723/wd/hub”
APPIUM_LOG_LEVEL = “debug”

# Android设备配置
ANDROID_DEVICE_NAME = “emulator-5554”
ANDROID_PLATFORM_VERSION = “13”
ANDROID_APP_PATH = os.path.join(os.path.dirname(__file__), “app”, “demo.apk”)
ANDROID_APP_PACKAGE = “com.example.demo”
ANDROID_APP_ACTIVITY = “com.example.demo.MainActivity”

# iOS设备配置
IOS_DEVICE_NAME = “iPhone 14 Pro”
IOS_PLATFORM_VERSION = “16.4”
IOS_BUNDLE_ID = “com.example.demo”
IOS_APP_PATH = os.path.join(os.path.dirname(__file__), “app”, “demo.ipa”)

# Selenium配置
CHROME_DRIVER_PATH = “/path/to/chromedriver”
FIREFOX_DRIVER_PATH = “/path/to/geckodriver”
SELENIUM_HUB_URL = “http://localhost:4444/wd/hub”

# 测试数据
TEST_USER = {“username”: “testuser”, “password”: “testpass123”}

config = AppConfig()
三、核心API与定位策略详解
3.1 Appium元素定位策略
Appium支持多种定位策略,与Selenium兼容但增加了移动端特有的定位方式。
完整定位策略对比表:
策略名称
Selenium
Appium
说明
ID
✅
✅
元素唯一标识符
Name
✅
�1
元素的name属性
Class Name
✅
✅
元素的class名称
CSS Selector
✅
✅
CSS选择器
XPath
✅
✅
XML路径语言
Accessibility ID
❌
✅
iOS/Android无障碍标识
Android UIAutomator
❌
✅
Android原生定位器
iOS NSPredicate
❌
✅
iOS原生谓词
Image
❌
✅
基于图像识别
Custom
❌
✅
自定义定位器
代码示例:
from appium import webdriver
from appium.webdriver.common.appiumby import AppiumBy

# 初始化驱动
driver = webdriver.Remote(
command_executor=config.APPIUM_SERVER_URL,
desired_capabilities={
“platformName”: “Android”,
“deviceName”: config.ANDROID_DEVICE_NAME,
“appPackage”: config.ANDROID_APP_PACKAGE,
“appActivity”: config.ANDROID_APP_ACTIVITY,
“automationName”: “UiAutomator2”,
“noReset”: True,
“fullReset”: False,
}
)

# 多种定位方式示例
element_by_id = driver.find_element(AppiumBy.ID, “com.example.demo:id/login_button”)
element_by_xpath = driver.find_element(Appby.XPATH, “//android.widget.Button[@text=’登录’]”)
element_by_accessibility_id = driver.find_element(AppiumBy.ACCESSIBILITY_ID, “login_button”)
element_by_uiautomator = driver.find_element(AppiumBy.ANDROID_UIAUTOMATOR, ‘new UiSelector().text(“登录”)’)
element_by_class = driver.find_element(AppiumBy.CLASS_NAME, “android.widget.EditText”)

# 多元素查找
elements = driver.find_elements(AppiumBy.CLASS_NAME, “android.widget.Button”)
3.2 Selenium元素定位策略
Selenium 4提供了强大的定位能力,特别是相对定位器。
完整定位示例:
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.keys import Keys

# 初始化浏览器驱动
driver = webdriver.Chrome(executable_path=config.CHROME_DRIVER_PATH)
driver.get(“https://example.com/login”)

# 基本定位策略
element_by_id = driver.find_element(By.ID, “username”)
element_by_name = driver.find_element(By.NAME, “password”)
element_by_xpath = driver.find_element(By.XPATH, “//input[@type=’submit’]”)
element_by_css = driver.find_element(By.CSS_SELECTOR, “button[type=’submit’]”)
element_by_link_text = driver.find_element(By.LINK_TEXT, “Forgot Password?”)

# Selenium 4 相对定位器
from selenium.webdriver.support.relative_locator import locate_with

submit_button = driver.find_element(By.CSS_SELECTOR, “button[type=’submit’]”)
# 找到提交按钮上方的元素
username_input = driver.find_element(locate_with(By.TAG_NAME, “input”).above(submit_button))
# 找到密码输入框右侧的元素
remember_me = driver.find_element(locate_with(By.ID, “remember”).to_right_of(username_input))

# 等待策略(重要!)
wait = WebDriverWait(driver, 10)
element = wait.until(
EC.presence_of_element_located((By.ID, “dynamic_element”))
)

# 高级交互
actions = ActionChains(driver)
actions.move_to_element(element).click().perform()

# 执行JavaScript
driver.execute_script(“window.scrollTo(0, document.body.scrollHeight);”)
3.3 混合应用与Webview处理
对于混合应用,需要在原生上下文和Webview之间切换:
# 获取当前所有上下文
contexts = driver.contexts
print(f”可用上下文: {contexts}”)
# 输出示例: [‘NATIVE_APP’, ‘WEBVIEW_com.example.demo’]

# 切换到Webview
if len(contexts) > 1:
driver.switch_to.context(contexts[1]) # 切换到第一个Webview

# 现在可以使用Selenium方式操作Web元素
web_element = driver.find_element(By.CSS_SELECTOR, “#web_login_button”)
web_element.click()

# 切换回原生上下文
driver.switch_to.context(‘NATIVE_APP’)
四、解决兼容性难题的实战技巧
4.1 多设备/多平台测试策略
使用Appium Grid进行分布式测试:
# 启动Appium Hub
appium –port 4444 –hub http://localhost:4444

# 启动多个Node连接到Hub
appium –port 4723 –hub http://localhost:4444/wd/hub –node-config ./node1.config.json
appium –port 4724 –hub http://localhost:4444/wd/hub –node-config ./node2.config.json
node.config.json示例:
{
“capabilities”: [
{
“platformName”: “Android”,
“deviceName”: “Pixel_5”,
“platformVersion”: “13”,
“maxInstances”: 3
},
{
“platformName”: “iOS”,
“deviceName”: “iPhone_14”,
“platformVersion”: “16.4”,
“maxInstances”: 2
}
],
“configuration”: {
“cleanUpCycle”: 2000,
“timeout”: 30000,
“proxy”: “org.openqa.grid.selenium.proxy.DefaultRemoteProxy”,
“maxSession”: 5,
“register”: true,
“registerCycle”: 5000,
“hubPort”: 4444,
“hubHost”: “localhost”
}
}
Python测试脚本动态选择设备:
import subprocess
import json
from typing import List, Dict

class DeviceManager:
“””设备管理器,用于动态获取可用设备”””

def get_connected_android_devices(self) -> List[str]:
“””获取连接的Android设备列表”””
result = subprocess.run([‘adb’, ‘devices’], capture_output=True, text=True)
devices = []
for line in result.stdout.split(‘\n’)[1:]:
if ‘\tdevice’ in line:
devices.append(line.split(‘\t’)[0])
return devices

def get_device_info(self, device_id: str) -> Dict:
“””获取设备详细信息”””
# 获取设备型号
model = subprocess.run([‘adb’, ‘-s’, device_id, ‘shell’, ‘getprop’, ‘ro.product.model’],
capture_output=True, text=True).stdout.strip()
# 获取系统版本
version = subprocess.run([‘adb’, ‘-s’, device_id, ‘shell’, ‘getprop’, ‘ro.build.version.release’],
capture_output=True, text=True).stdout.strip()
# 获取屏幕分辨率
resolution = subprocess.run([‘adb’, ‘-s’, device_id, ‘shell’, ‘wm’, ‘size’],
capture_output=True, text=True).stdout.strip()

return {
“deviceName”: device_id,
“platformVersion”: version,
“model”: model,
“resolution”: resolution
}

def generate_desired_capabilities(self, device_info: Dict, app_path: str) -> Dict:
“””根据设备信息生成desired capabilities”””
return {
“platformName”: “Android”,
“deviceName”: device_info[“deviceName”],
“platformVersion”: device_info[“platformVersion”],
“app”: app_path,
“automationName”: “UiAutomator2”,
“noReset”: True,
“fullReset”: False,
“newCommandTimeout”: 300,
“autoGrantPermissions”: True,
“autoAcceptAlerts”: True,
# 性能优化
“disableWindowAnimation”: True,
“noSign”: True,
“skipUnlock”: True
}

# 使用示例
device_manager = DeviceManager()
devices = device_manager.get_connected_android_devices()

for device_id in devices:
device_info = device_manager.get_device_info(device_id)
caps = device_manager.generate_desired_capabilities(device_info, config.ANDROID_APP_PATH)

# 为每个设备创建独立的测试会话
driver = webdriver.Remote(config.APPIUM_SERVER_URL, caps)
# 执行测试…
4.2 处理动态内容与异步加载
显式等待与自定义等待条件:
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException

class CustomWaitConditions:
“””自定义等待条件”””

@staticmethod
def element_has_non_empty_text(locator):
“””等待元素文本不为空”””
def _predicate(driver):
element = driver.find_element(*locator)
text = element.text.strip()
return element if text else False
return _predicate

@staticmethod
def element_is_clickable(locator):
“””元素可点击(存在、可见且启用)”””
def _predicate(driver):
element = driver.find_element(*locator)
if element.is_displayed() and element.is_enabled():
return element
return False
return _predicate

@staticmethod
def list_has_items(locator, min_count=1):
“””等待列表元素数量达到最小值”””
def _predicate(driver):
elements = driver.find_elements(*locator)
return elements if len(elements) >= min_count else False
return _predicate

# 使用自定义等待
wait = WebDriverWait(driver, 15)
try:
# 等待列表至少有3个项目
items = wait.until(CustomWaitConditions.list_has_items((By.CLASS_NAME, “list-item”), 3))

# 等待元素有非空文本
dynamic_text = wait.until(
CustomWaitConditions.element_has_non_empty_text((By.ID, “status_text”))
)
except TimeoutException:
print(“等待超时,元素未达到条件”)
处理AJAX加载:
def wait_for_ajax_complete(driver, timeout=10):
“””等待所有AJAX请求完成”””
wait = WebDriverWait(driver, timeout)
wait.until(lambda d: d.execute_script(“return jQuery.active == 0”) if d.execute_script(“return typeof jQuery !== ‘undefined’”) else True)
wait.until(lambda d: d.execute_script(“return document.readyState === ‘complete’”))

def wait_for_network_idle(driver, timeout=10, idle_time=2):
“””等待网络空闲(无新请求)”””
import time
last_activity = time.time()
while time.time() – last_activity < idle_time: pending = driver.execute_script("return window.performance.getEntriesByType('resource').filter(r => r.responseEnd === 0).length”)
if pending == 0:
last_activity = time.time()
time.sleep(0.1)
if time.time() – last_activity > timeout:
raise TimeoutException(“Network idle timeout”)
4.3 跨浏览器/跨设备兼容性测试
使用Selenium Grid进行跨浏览器测试:
from selenium import webdriver
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities

class CrossBrowserTest:
“””跨浏览器测试管理器”””

BROWSER_CONFIGS = {
“chrome”: {
“browserName”: “chrome”,
“version”: “118.0”,
“platform”: “WINDOWS”,
“goog:chromeOptions”: {
“args”: [“–headless”, “–no-sandbox”, “–disable-dev-shm-usage”]
}
},
“firefox”: {
“browserName”: “firefox”,
“version”: “118.0”,
“platform”: “WINDOWS”,
“moz:firefoxOptions”: {
“args”: [“–headless”]
}
},
“safari”: {
“browserName”: “safari”,
“version”: “16.0”,
“platform”: “MACOS”
}
}

def __init__(self, hub_url=”http://localhost:4444/wd/hub”):
self.hub_url = hub_url

def create_driver(self, browser_name: str) -> webdriver.Remote:
“””为指定浏览器创建驱动实例”””
if browser_name not in self.BROWSER_CONFIGS:
raise ValueError(f”Unsupported browser: {browser_name}”)

capabilities = self.BROWSER_CONFIGS[browser_name].copy()
return webdriver.Remote(
command_executor=self.hub_url,
desired_capabilities=capabilities
)

def run_cross_browser_test(self, test_func):
“””装饰器:在所有配置的浏览器上运行测试”””
def wrapper():
results = {}
for browser_name in self.BROWSER_CONFIGS.keys():
driver = None
try:
driver = self.create_driver(browser_name)
test_func(driver)
results[browser_name] = “PASSED”
except Exception as e:
results[browser_name] = f”FAILED: {str(e)}”
finally:
if driver:
driver.quit()

# 打印结果
print(“\n=== Cross Browser Test Results ===”)
for browser, result in results.items():
print(f”{browser}: {result}”)

return wrapper

# 使用示例
cross_browser = CrossBrowserTest()

@cross_browser.run_cross_browser_test
def test_login_functionality(driver):
“””测试登录功能”””
driver.get(“https://example.com/login”)
driver.find_element(By.ID, “username”).send_keys(“testuser”)
driver.find_element(By.ID, “password”).send_keys(“testpass”)
driver.find_element(By.ID, “login_button”).click()
# 验证登录成功
assert “dashboard” in driver.current_url
4.4 处理移动端特有兼容性问题
屏幕尺寸与分辨率适配:
def get_screen_info(driver):
“””获取设备屏幕信息”””
if driver.capabilities[‘platformName’] == ‘Android’:
# 获取屏幕尺寸
size = driver.get_window_size()
# 获取设备密度
density = driver.execute_script(“return window.devicePixelRatio”)
return {“width”: size[‘width’], “height”: size[‘height’], “density”: density}
return {}

def adaptive_element_click(driver, locator, target_ratio=0.8):
“””自适应点击:根据屏幕尺寸调整点击位置”””
screen_info = get_screen_info(driver)
if not screen_info:
return

# 计算安全点击区域(避免边缘)
safe_width = screen_info[‘width’] * target_ratio
safe_height = screen_info[‘height’] * target_ratio
offset_x = (screen_info[‘width’] – safe_width) / 2
offset_y = (screen_info[‘height’] – safe_height) / 2

# 使用ActionChains进行精确点击
from selenium.webdriver.common.action_chains import ActionChains
element = driver.find_element(*locator)
actions = ActionChains(driver)
actions.move_to_element_with_offset(element, offset_x, offset_y).click().perform()

def handle_orientation_change(driver):
“””处理屏幕旋转”””
# 获取当前方向
current_orientation = driver.orientation
print(f”当前方向: {current_orientation}”)

# 旋转到横屏
driver.orientation = “LANDSCAPE”
driver.implicitly_wait(2) # 等待重绘

# 旋转回竖屏
driver.orientation = “PORTRAIT”
driver.implicitly_wait(2)
不同Android版本的权限处理:
def handle_android_permissions(driver, permissions: list):
“””自动处理Android权限弹窗(适用于不同版本)”””
# Android 11+ 使用权限弹窗处理
if driver.capabilities[‘platformVersion’] >= ’11’:
for permission in permissions:
try:
# 使用UiAutomator定位权限按钮
allow_button = driver.find_element(
AppiumBy.ANDROID_UIAUTOMATOR,
‘new UiSelector().textMatches(“(允许|Allow|好|OK)”)’
)
allow_button.click()
except:
pass
else:
# Android 10及以下:提前设置权限
for permission in permissions:
driver.execute_script(‘mobile: changePermissions’, {
‘action’: ‘grant’,
‘appPackage’: config.ANDROID_APP_PACKAGE,
‘permissions’: permission
})

# 使用示例
handle_android_permissions(driver, [‘android.permission.CAMERA’, ‘android.permission.READ_EXTERNAL_STORAGE’])
5. 效率提升的高级技巧
5.1 测试代码模块化与Page Object模式
Page Object模式实现:
from abc import ABC, abstractmethod
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException, NoSuchElementException

class BasePage(ABC):
“””所有页面对象的基类”””

def __init__(self, driver, timeout=10):
self.driver = driver
self.wait = WebDriverWait(driver, timeout)
self._validate_page()

@abstractmethod
def _validate_page(self):
“””验证是否在正确的页面”””
pass

def find_element(self, locator, timeout=None):
“””安全查找元素”””
wait_time = timeout or self.wait._timeout
try:
return WebDriverWait(self.driver, wait_time).until(
EC.presence_of_element_located(locator)
)
except TimeoutException:
raise NoSuchElementException(f”Element {locator} not found within {wait_time} seconds”)

def click_element(self, locator, timeout=None):
“””安全点击元素”””
element = self.find_element(locator, timeout)
try:
element.click()
except:
# 备用方案:使用JavaScript点击
self.driver.execute_script(“arguments[0].click();”, element)

def send_keys(self, locator, text, timeout=None):
“””安全输入文本”””
element = self.find_element(locator, timeout)
element.clear()
element.send_keys(text)

def is_element_visible(self, locator, timeout=2):
“””检查元素是否可见(快速检查)”””
try:
WebDriverWait(self.driver, timeout).until(
EC.visibility_of_element_located(locator)
)
return True
except TimeoutException:
return False

# 登录页面对象
class LoginPage(BasePage):
“””登录页面”””

# 元素定位器
USERNAME_INPUT = (AppiumBy.ID, “com.example.demo:id/username”) if config.is_mobile else (By.ID, “username”)
PASSWORD_INPUT = (AppiumBy.ID, “com.example.demo:id/password”) if config.is_mobile else (By.ID, “password”)
LOGIN_BUTTON = (AppiumBy.ID, “com.example.demo:id/login_button”) if config.is_mobile else (By.ID, “login_button”)
ERROR_MESSAGE = (AppiumBy.ID, “com.example.demo:id/error_message”) if config.is_mobile else (By.ID, “error_message”)

def _validate_page(self):
“””验证登录页面元素存在”””
self.find_element(self.USERNAME_INPUT)

def login(self, username: str, password: str):
“””执行登录操作”””
self.send_keys(self.USERNAME_INPUT, username)
self.send_keys(self.PASSWORD_INPUT, password)
self.click_element(self.LOGIN_BUTTON)
return DashboardPage(self.driver) # 返回下一个页面对象

def get_error_message(self) -> str:
“””获取错误信息”””
if self.is_element_visible(self.ERROR_MESSAGE, timeout=2):
return self.find_element(self.ERROR_MESSAGE).text
return “”

# 使用示例
def test_login(driver):
login_page = LoginPage(driver)
dashboard = login_page.login(“testuser”, “testpass”)
assert dashboard.is_element_visible(dashboard.WELCOME_MESSAGE)
5.2 并行执行与测试分发
使用pytest-xdist实现并行测试:
# conftest.py
import pytest
from appium import webdriver
from selenium import webdriver as selenium_driver

@pytest.fixture(scope=”function”)
def appium_driver(request):
“””Appium驱动 fixture”””
platform = request.param # 从测试参数获取平台

if platform == “android”:
caps = {
“platformName”: “Android”,
“deviceName”: “emulator-5554”,
“appPackage”: config.ANDROID_APP_PACKAGE,
“appActivity”: config.ANDROID_APP_ACTIVITY,
“automationName”: “UiAutomator2”,
“noReset”: True
}
driver = webdriver.Remote(config.APPIUM_SERVER_URL, caps)
elif platform == “ios”:
caps = {
“platformName”: “iOS”,
“deviceName”: “iPhone 14 Pro”,
“bundleId”: config.IOS_BUNDLE_ID,
“automationName”: “XCUITest”,
“noReset”: True
}
driver = webdriver.Remote(config.APPIUM_SERVER_URL, caps)

yield driver
driver.quit()

@pytest.fixture(scope=”function”)
def selenium_driver():
“””Selenium驱动 fixture”””
driver = selenium_driver.Chrome(executable_path=config.CHROME_DRIVER_PATH)
yield driver
driver.quit()

# 并行测试示例
import pytest

@pytest.mark.parametrize(“appium_driver”, [“android”, “ios”], indirect=True)
def test_mobile_login(appium_driver):
“””移动端登录测试(Android和iOS并行)”””
login_page = LoginPage(appium_driver)
dashboard = login_page.login(“testuser”, “testpass”)
assert dashboard.is_element_visible(dashboard.WELCOME_MESSAGE)

def test_web_login(selenium_driver):
“””Web端登录测试”””
login_page = LoginPage(selenium_driver)
dashboard = login_page.login(“testuser”, “testpass”)
assert dashboard.is_element_visible(dashboard.WELCOME_MESSAGE)

# 命令行执行:pytest -n 3 # 3个进程并行
自定义并行执行器:
import threading
import subprocess
from queue import Queue
import time

class ParallelTestRunner:
“””自定义并行测试执行器”””

def __init__(self, max_workers=3):
self.max_workers = max_workers
self.task_queue = Queue()
self.results = []
self.lock = threading.Lock()

def add_test(self, test_command: str, test_name: str):
“””添加测试任务”””
self.task_queue.put({“command”: test_command, “name”: test_name})

def worker(self):
“””工作线程”””
while True:
try:
task = self.task_queue.get_nowait()
except:
break

start_time = time.time()
try:
result = subprocess.run(
task[“command”].split(),
capture_output=True,
text=True,
timeout=600
)
duration = time.time() – start_time

with self.lock:
self.results.append({
“name”: task[“name”],
“status”: “PASSED” if result.returncode == 0 else “FAILED”,
“duration”: duration,
“output”: result.stdout
})
except subprocess.TimeoutExpired:
with self.lock:
self.results.append({
“name”: task[“name”],
“status”: “TIMEOUT”,
“duration”: 600,
“output”: “Test execution timeout”
})
finally:
self.task_queue.task_done()

def run(self):
“””启动并行执行”””
threads = []
for _ in range(self.max_workers):
t = threading.Thread(target=self.worker)
t.start()
threads.append(t)

self.task_queue.join()

for t in threads:
t.join()

return self.results

# 使用示例
runner = ParallelTestRunner(max_workers=3)
runner.add_test(“pytest tests/test_android_login.py”, “Android Login”)
runner.add_test(“pytest tests/test_ios_login.py”, “iOS Login”)
runner.add_test(“pytest tests/test_web_login.py”, “Web Login”)

results = runner.run()
for r in results:
print(f”{r[‘name’]}: {r[‘status’]} ({r[‘duration’]:.2f}s)”)
5.3 测试数据管理与工厂模式
使用工厂模式生成测试数据:
import random
import string
from datetime import datetime, timedelta
from typing import Dict, List

class TestDataFactory:
“””测试数据工厂”””

@staticmethod
def generate_user_data(count: int = 1) -> List[Dict]:
“””生成用户测试数据”””
users = []
for i in range(count):
users.append({
“username”: f”testuser_{random.randint(1000, 9999)}”,
“email”: f”test{random.randint(1000, 9999)}@example.com”,
“password”: ”.join(random.choices(string.ascii_letters + string.digits, k=12)),
“first_name”: random.choice([“John”, “Jane”, “Bob”, “Alice”]),
“last_name”: random.choice([“Smith”, “Johnson”, “Williams”, “Brown”]),
“age”: random.randint(18, 65)
})
return users

@staticmethod
def generate_product_data(count: int = 1) -> List[Dict]:
“””生成产品测试数据”””
products = []
for i in range(count):
products.append({
“name”: f”Product_{random.randint(1000, 9999)}”,
“price”: round(random.uniform(10.0, 1000.0), 2),
“category”: random.choice([“Electronics”, “Clothing”, “Books”, “Food”]),
“sku”: f”SKU{random.randint(10000, 99999)}”,
“stock”: random.randint(0, 100)
})
return products

@staticmethod
def generate_date_range(start_days_ago: int = 30, end_days_ahead: int = 30) -> List[str]:
“””生成日期范围”””
start_date = datetime.now() – timedelta(days=start_days_ago)
end_date = datetime.now() + timedelta(days=end_days_ahead)

dates = []
current = start_date
while current <= end_date: dates.append(current.strftime("%Y-%m-%d")) current += timedelta(days=1) return dates @staticmethod def generate_search_queries(count: int = 10) -> List[str]:
“””生成搜索查询”””
base_queries = [“laptop”, “phone”, “book”, “shirt”, “shoes”, “watch”, “bag”, “camera”]
queries = []
for _ in range(count):
base = random.choice(base_queries)
modifier = random.choice([“cheap”, “best”, “new”, “used”, “refurbished”, “premium”])
queries.append(f”{modifier} {base}”)
return queries

# 使用示例
factory = TestDataFactory()
users = factory.generate_user_data(5)
products = factory.generate_product_data(3)
dates = factory.generate_date_range(7, 7)
search_queries = factory.generate_search_queries(5)

# 在测试中使用
def test_bulk_user_registration(driver):
“””批量用户注册测试”””
login_page = LoginPage(driver)
registration_page = login_page.goto_registration()

for user in factory.generate_user_data(3):
registration_page.register(user)
assert registration_page.is_success_message_visible()
5.4 测试报告与可视化
使用Allure生成详细报告:
import allure
import pytest

@allure.feature(“登录功能”)
class TestLogin:

@allure.story(“成功登录”)
@allure.description(“使用有效凭据登录应成功”)
@allure.severity(allure.severity_level.CRITICAL)
@pytest.mark.parametrize(“user”, TestDataFactory.generate_user_data(1))
def test_successful_login(self, appium_driver, user):
“””测试成功登录”””
with allure.step(“输入用户名”):
allure.attach(f”用户名: {user[‘username’]}”, “用户名”, allure.attachment_type.TEXT)
appium_driver.find_element(AppiumBy.ID, “username”).send_keys(user[‘username’])

with allure.step(“输入密码”):
appium_driver.find_element(AppiumBy.ID, “password”).send_keys(user[‘password’])

with allure.step(“点击登录按钮”):
appium_driver.find_element(AppiumBy.ID, “login_button”).click()

with allure.step(“验证登录成功”):
welcome = appium_driver.find_element(AppiumBy.ID, “welcome_message”)
assert welcome.is_displayed()
allure.attach(f”欢迎信息: {welcome.text}”, “验证结果”, allure.attachment_type.TEXT)

@allure.story(“失败登录”)
@allure.description(“使用无效凭据登录应显示错误”)
def test_failed_login(self, appium_driver):
“””测试失败登录”””
login_page = LoginPage(appium_driver)
login_page.login(“invalid_user”, “wrong_pass”)

error_msg = login_page.get_error_message()
assert “Invalid credentials” in error_msg

# 截图作为附件
screenshot = appium_driver.get_screenshot_as_png()
allure.attach(screenshot, “错误截图”, allure.attachment_type.PNG)

# 生成报告命令
# pytest –alluredir=./allure-results
# allure serve ./allure-results
自定义HTML报告生成器:
import json
from datetime import datetime

class HTMLReportGenerator:
“””自定义HTML报告生成器”””

def __init__(self, output_file=”test_report.html”):
self.output_file = output_file
self.test_results = []

def add_test_result(self, test_name: str, status: str, duration: float, error: str = None, screenshot: str = None):
“””添加测试结果”””
self.test_results.append({
“name”: test_name,
“status”: status,
“duration”: duration,
“error”: error,
“screenshot”: screenshot,
“timestamp”: datetime.now().isoformat()
})

def generate(self):
“””生成HTML报告”””
html_content = f”””



Test Report


Test Execution Report

Generated: {datetime.now().strftime(“%Y-%m-%d %H:%M:%S”)}

Total: {len(self.test_results)}, Passed: {sum(1 for t in self.test_results if t[‘status’] == ‘PASSED’)}, Failed: {sum(1 for t in self.test_results if t[‘status’] == ‘FAILED’)}

“””

for test in self.test_results:
html_content += f”””

{test[‘name’]} – {test[‘status’]} ({test[‘duration’]:.2f}s)

Timestamp: {test[‘timestamp’]}

“””
if test[‘error’]:
html_content += f”

{test[‘error’]}

”
if test[‘screenshot’]:
html_content += f”

”
html_content += “

”

html_content += “””



“””

with open(self.output_file, ‘w’) as f:
f.write(html_content)

print(f”Report generated: {self.output_file}”)

# 使用示例
report = HTMLReportGenerator()
report.add_test_result(“Android Login”, “PASSED”, 5.2)
report.add_test_result(“iOS Login”, “FAILED”, 3.1, “Element not found”, “screenshot.png”)
report.generate()
6. 实战案例:完整测试流程
6.1 案例:电商应用跨平台测试
场景:测试电商应用的商品搜索、加入购物车、结算流程
import pytest
import allure
from appium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

class ECommerceAppTest:
“””电商应用测试类”””

def setup_method(self, method):
“””每个测试方法前执行”””
# 根据测试方法名判断平台
if “android” in method.__name__:
self.driver = self.setup_android_driver()
elif “ios” in method.__name__:
self.driver = self.setup_ios_driver()
else:
self.driver = self.setup_web_driver()

self.wait = WebDriverWait(self.driver, 10)

def teardown_method(self):
“””每个测试方法后执行”””
if hasattr(self, ‘driver’):
self.driver.quit()

def setup_android_driver(self):
“””配置Android驱动”””
caps = {
“platformName”: “Android”,
“deviceName”: “emulator-5554”,
“appPackage”: “com.ecommerce.app”,
“appActivity”: “com.ecommerce.app.MainActivity”,
“automationName”: “UiAutomator2”,
“noReset”: True,
“autoGrantPermissions”: True
}
return webdriver.Remote(config.APPIUM_SERVER_URL, caps)

def setup_ios_driver(self):
“””配置iOS驱动”””
caps = {
“platformName”: “iOS”,
“deviceName”: “iPhone 14 Pro”,
“bundleId”: “com.ecommerce.app”,
“automationName”: “XCUITest”,
“noReset”: True
}
return webdriver.Remote(config.APPIUM_SERVER_URL, caps)

def setup_web_driver(self):
“””配置Web驱动”””
options = webdriver.ChromeOptions()
options.add_argument(“–headless”)
options.add_argument(“–no-sandbox”)
options.add_argument(“–disable-dev-shm-usage”)
return webdriver.Chrome(executable_path=config.CHROME_DRIVER_PATH, options=options)

@allure.feature(“商品搜索”)
@pytest.mark.parametrize(“query”, [“laptop”, “phone”, “book”])
def test_search_product(self, query):
“””测试商品搜索功能”””
with allure.step(f”搜索商品: {query}”):
# 点击搜索框
search_box = self.wait.until(
EC.element_to_be_clickable((AppiumBy.ACCESSIBILITY_ID, “search_input”))
)
search_box.click()
search_box.send_keys(query)

# 点击搜索按钮
search_button = self.driver.find_element(AppiumBy.ACCESSIBILITY_ID, “search_button”)
search_button.click()

# 等待结果加载
results = self.wait.until(
EC.presence_of_all_elements_located((AppiumBy.CLASS_NAME, “product_item”))
)

allure.attach(f”找到 {len(results)} 个商品”, “搜索结果”, allure.attachment_type.TEXT)
assert len(results) > 0, f”未找到商品: {query}”

@allure.feature(“购物车”)
def test_add_to_cart_android(self):
“””Android端:添加商品到购物车”””
# 搜索商品
self.test_search_product(“laptop”)

# 选择第一个商品
first_product = self.driver.find_element(AppiumBy.CLASS_NAME, “product_item”)
first_product.click()

# 点击加入购物车
add_to_cart = self.wait.until(
EC.element_to_be_clickable((AppiumBy.ID, “com.ecommerce.app:id/add_to_cart_button”))
)
add_to_cart.click()

# 验证提示信息
toast = self.driver.find_element(AppiumBy.XPATH, “//*[contains(@text, ‘已加入购物车’)]”)
assert toast.is_displayed()

@allure.feature(“购物车”)
def test_add_to_cart_ios(self):
“””iOS端:添加商品到购物车”””
# iOS特定的定位方式
self.test_search_product(“phone”)

first_product = self.driver.find_element(AppiumBy.IOS_PREDICATE, “type == ‘XCUIElementTypeCell’”)
first_product.click()

# iOS使用不同的按钮ID
add_to_cart = self.wait.until(
EC.element_to_be_clickable((AppiumBy.ACCESSIBILITY_ID, “Add to Cart”))
)
add_to_cart.click()

# 验证购物车数量
cart_badge = self.driver.find_element(AppiumBy.ACCESSIBILITY_ID, “cart_badge”)
assert cart_badge.text == “1”

@allure.feature(“结算流程”)
def test_checkout_flow(self):
“””测试完整结算流程”””
# 1. 添加商品到购物车
self.test_add_to_cart_android()

# 2. 进入购物车
cart_icon = self.driver.find_element(AppiumBy.ACCESSIBILITY_ID, “cart_tab”)
cart_icon.click()

# 3. 点击结算
checkout_button = self.wait.until(
EC.element_to_be_clickable((AppiumBy.ID, “com.ecommerce.app:id/checkout_button”))
)
checkout_button.click()

# 4. 填写配送信息
address_input = self.driver.find_element(AppiumBy.ID, “com.ecommerce.app:id/address_input”)
address_input.send_keys(“123 Main St, City”)

# 5. 选择支付方式
payment_option = self.driver.find_element(AppiumBy.XPATH, “//android.widget.RadioButton[@text=’Credit Card’]”)
payment_option.click()

# 6. 确认订单
confirm_button = self.driver.find_element(AppiumBy.ID, “com.ecommerce.app:id/confirm_order”)
confirm_button.click()

# 7. 验证订单成功
success_message = self.wait.until(
EC.presence_of_element_located((AppiumBy.ID, “com.ecommerce.app:id/success_message”))
)
assert “Order Confirmed” in success_message.text

# 截图记录
screenshot = self.driver.get_screenshot_as_png()
allure.attach(screenshot, “订单确认截图”, allure.attachment_type.PNG)

# 执行测试
if __name__ == “__main__”:
# 运行特定平台的测试
pytest.main([__file__, “-v”, “-k=test_add_to_cart_android”, “–alluredir=./allure-results”])
6.2 性能监控与优化
监控测试执行性能:
import time
import psutil
from functools import wraps
from contextlib import contextmanager

class PerformanceMonitor:
“””性能监控器”””

def __init__(self):
self.metrics = {}

@contextmanager
def measure(self, operation_name: str):
“””测量代码块执行时间”””
start_time = time.time()
start_memory = psutil.Process().memory_info().rss / 1024 / 1024 # MB

try:
yield
finally:
end_time = time.time()
end_memory = psutil.Process().memory_info().rss / 1024 / 1024

duration = end_time – start_time
memory_used = end_memory – start_memory

self.metrics[operation_name] = {
“duration”: duration,
“memory_used”: memory_used
}

print(f”[{operation_name}] Duration: {duration:.2f}s, Memory: {memory_used:.2f}MB”)

def get_report(self):
“””生成性能报告”””
total_time = sum(m[“duration”] for m in self.metrics.values())
total_memory = sum(m[“memory_used”] for m in self.metrics.values())

report = “=== Performance Report ===\n”
for name, metrics in self.metrics.items():
report += f”{name}: {metrics[‘duration’]:.2f}s, {metrics[‘memory_used’]:.2f}MB\n”
report += f”Total: {total_time:.2f}s, {total_memory:.2f}MB”

return report

# 使用示例
monitor = PerformanceMonitor()

def test_with_monitoring(driver):
“””带性能监控的测试”””
with monitor.measure(“Search Operation”):
search_box = driver.find_element(AppiumBy.ACCESSIBILITY_ID, “search_input”)
search_box.send_keys(“laptop”)
driver.find_element(AppiumBy.ACCESSIBILITY_ID, “search_button”).click()

with monitor.measure(“Load Results”):
wait = WebDriverWait(driver, 10)
wait.until(EC.presence_of_all_elements_located((AppiumBy.CLASS_NAME, “product_item”)))

print(monitor.get_report())
7. 常见问题与解决方案
7.1 Appium常见问题
问题1:元素定位失败
# 解决方案:使用多种定位策略组合
def find_element_with_fallback(driver, locators: list, timeout=10):
“””多种定位策略组合查找”””
wait = WebDriverWait(driver, timeout)
for locator in locators:
try:
return wait.until(EC.presence_of_element_located(locator))
except:
continue
raise Exception(f”无法找到元素,尝试了: {locators}”)

# 使用
element = find_element_with_fallback(driver, [
(AppiumBy.ID, “element_id”),
(AppiumBy.ACCESSIBILITY_ID, “element_accessibility_id”),
(AppiumBy.XPATH, “//android.widget.Button[@text=’Submit’]”),
(AppiumBy.CLASS_NAME, “android.widget.Button”)
])
问题2:WebView内容无法定位
# 解决方案:确保切换到正确的WebView上下文
def switch_to_webview(driver, timeout=10):
“””切换到WebView并等待加载”””
wait = WebDriverWait(driver, timeout)

# 等待WebView出现
def has_webview(d):
contexts = d.contexts
return len(contexts) > 1

wait.until(has_webview)

# 获取WebView上下文
contexts = driver.contexts
webview_context = None

for context in contexts:
if context.startswith(‘WEBVIEW_’):
webview_context = context
break

if webview_context:
driver.switch_to.context(webview_context)
# 等待页面加载
wait.until(lambda d: d.execute_script(“return document.readyState”) == “complete”)
return True

return False
问题3:iOS模拟器启动慢
# 解决方案:使用预启动的模拟器
import subprocess

def prelaunch_ios_simulator():
“””预启动iOS模拟器”””
# 查找可用的模拟器
result = subprocess.run([‘xcrun’, ‘simctl’, ‘list’, ‘devices’, ‘iOS’, ‘|’, ‘grep’, ‘Booted’],
capture_output=True, text=True, shell=True)

if “Booted” not in result.stdout:
# 启动模拟器
subprocess.run([‘xcrun’, ‘simctl’, ‘boot’, ‘iPhone_14_Pro’], check=True)

# 等待就绪
time.sleep(10)

# 在测试配置中使用
@pytest.fixture(scope=”session”, autouse=True)
def ios_simulator_setup():
if config.PLATFORM == “ios”:
prelaunch_ios_simulator()
7.2 Selenium常见问题
问题1:元素在视口外
# 解决方案:滚动到元素可见
def scroll_to_element(driver, element):
“””滚动到元素可见”””
driver.execute_script(“arguments[0].scrollIntoView({behavior: ‘smooth’, block: ‘center’});”, element)
time.sleep(0.5) # 等待滚动完成

# 或者使用ActionChains
from selenium.webdriver.common.action_chains import ActionChains
def scroll_to_element_actions(driver, element):
“””使用ActionChains滚动”””
actions = ActionChains(driver)
actions.move_to_element(element).perform()
问题2:iframe切换问题
# 解决方案:安全切换iframe
def switch_to_iframe(driver, frame_locator, timeout=10):
“””安全切换到iframe”””
wait = WebDriverWait(driver, timeout)
frame = wait.until(EC.frame_to_be_available_and_switch_to_it(frame_locator))
return frame

# 使用后切回默认内容
def switch_back_to_default(driver):
“””切回默认内容”””
driver.switch_to.default_content()
8. 总结与最佳实践
8.1 核心最佳实践
始终使用显式等待
:避免隐式等待和固定sleep
Page Object模式
:提高代码可维护性
配置管理
:统一管理环境参数
并行执行
:显著提升测试效率
数据驱动
:分离测试逻辑与测试数据
错误处理
:完善的异常捕获和重试机制
日志记录
:详细记录测试执行过程
截图/录屏
:失败时自动记录现场
8.2 效率提升检查清单
[ ] 使用Page Object模式组织代码
[ ] 实现配置管理,支持多环境
[ ] 使用并行测试执行
[ ] 集成Allure或类似报告工具
[ ] 实现失败重试机制
[ ] 使用数据工厂生成测试数据
[ ] 监控测试性能指标
[ ] 建立元素定位器缓存机制
[ ] 实现跨平台测试覆盖
[ ] 持续集成(CI/CD)集成
8.3 持续学习资源
官方文档
:Appium.io, Selenium.dev
社区
:Appium Discuss, Stack Overflow
GitHub
:关注Appium和Selenium的最新更新
博客
:持续关注行业最佳实践
会议
:参加Selenium Conf, Appium Conf
通过掌握这些实战技巧,你将能够构建高效、稳定、可维护的跨平台自动化测试框架,有效解决兼容性难题,并显著提升测试效率。记住,自动化测试是一个持续优化的过程,不断学习和改进是关键。