Python+Appium移动端自动化测试:从环境搭建到Page Object框架实战
1. 项目概述:为什么选择Python+Appium?
如果你正在为移动端应用的质量保障发愁,或者厌倦了手动重复点击、滑动、输入的操作,那么今天聊的这个组合,绝对能让你眼前一亮。Python + Appium,这几乎是目前移动端自动化测试领域最主流、最灵活、也最受开发者欢迎的技术栈。我见过不少团队,从零开始搭建,一周内就能跑通核心业务流程的自动化脚本,效率提升立竿见影。
简单来说,Appium是一个开源的、跨平台的移动应用自动化测试框架。它的核心魅力在于“一次编写,到处运行”——你可以用同一套API和脚本,去测试iOS、Android甚至Windows平台上的原生、混合或移动Web应用。而Python,以其简洁的语法、丰富的生态和强大的社区支持,成为了驱动Appium脚本的绝佳语言。两者结合,你不需要去啃Java或JavaScript,用你熟悉的Python就能快速上手,把测试工程师从繁琐的重复劳动中解放出来,专注于更复杂的测试场景设计和问题分析。
这个框架适合谁?无论是刚入行的测试新人,想系统学习自动化测试;还是有一定经验的测试开发,希望优化现有流程;甚至是开发同学,想为自己的应用快速构建一套冒烟测试集,Python+Appium都是一个极佳的起点。它降低了自动化的门槛,让你能把精力集中在“测什么”和“怎么测得好”上,而不是纠结于“怎么写”和“怎么跑不通”。
2. 框架核心设计与环境搭建全攻略
搭建一个稳定可用的自动化测试环境,是成功的第一步,也是最容易踩坑的一步。很多人在这里耗费大量时间,最终因为环境问题而放弃。我将按照“最小化依赖”和“版本对齐”的原则,带你一步步搭建,并解释每个环节的必要性。
2.1 环境准备与依赖安装
首先,我们需要一个清晰的清单。一个完整的Python+Appium测试环境,主要包含三大部分:编程语言环境(Python)、测试框架与驱动(Appium相关)、移动设备环境(Android SDK/iOS开发工具)。为了聚焦和普适性,我们以Android平台为例进行说明,iOS的思路类似,但需要Mac环境和Xcode。
1. Python环境安装与配置: 这是我们的脚本运行基础。强烈建议使用Python 3.7及以上版本,因为很多新的库对旧版本支持不佳。不要去官网下载最新的3.12或3.13,选择3.8或3.9这类长期支持版本更为稳妥,生态兼容性最好。
- 安装 :从Python官网下载安装包,安装时务必勾选“Add Python to PATH”,这样可以在命令行直接使用
python和pip命令。 - 验证 :安装后打开命令行(CMD或PowerShell),输入
python --version和pip --version,能正确显示版本号即表示安装成功。 - 换源 :为了提高后续安装库的速度,建议将pip源更换为国内镜像。在用户目录下(
C:\Users\你的用户名\)新建一个pip文件夹,里面新建文件pip.ini,写入以下内容:[global] index-url = https://pypi.tuna.tsinghua.edu.cn/simple [install] trusted-host = pypi.tuna.tsinghua.edu.cn
2. Appium Server的安装: Appium Server是核心的“翻译官”和“调度中心”。它接收我们Python脚本发来的指令(基于WebDriver协议),并将其翻译成设备系统(UIAutomator2 for Android, XCUITest for iOS)能理解的命令。
- 方案选择 :你有两种选择。一是通过Node.js使用npm安装(
npm install -g appium),这种方式更“原生”,便于后续升级和自定义。二是直接下载Appium Desktop图形界面客户端,它内置了Server和元素定位工具Inspector,对新手更友好。我推荐新手从Appium Desktop开始,减少环境复杂度。 - 安装Appium Desktop :从Appium官网下载对应操作系统的安装包,直接安装即可。安装后打开,你会看到一个主机和端口设置界面,默认
localhost:4723,直接启动服务即可。
3. Android环境配置(针对Android测试): 这是最复杂的一环,核心是安装并配置Android SDK。
- 安装Android Studio :这是谷歌官方的IDE,它会帮我们管理SDK。安装时,在
Choose Components界面,确保勾选Android Virtual Device(用于创建模拟器)。 - 配置环境变量 :安装完成后,需要配置两个关键环境变量:
ANDROID_HOME:指向你的SDK安装路径,通常在C:\Users\你的用户名\AppData\Local\Android\Sdk。- 将
%ANDROID_HOME%\platform-tools和%ANDROID_HOME%\tools(或%ANDROID_HOME%\tools\bin)添加到系统的Path变量中。
- 验证 :打开新的命令行窗口,输入
adb version,如果能看到版本信息,说明ADB(Android调试桥)配置成功。输入avdmanager list,可以查看可用的系统镜像,为创建模拟器做准备。
4. 安装Python客户端库: 最后,我们需要在Python环境中安装与Appium Server通信的客户端库。
pip install Appium-Python-Client
这个库提供了所有与Appium交互的Python方法,是对Selenium Python库的扩展。
注意 :环境搭建最大的坑就是“版本冲突”。务必确保你的Appium Server版本、Appium-Python-Client版本、手机系统版本(或模拟器镜像版本)以及被测应用的兼容性。一个实用的技巧是,在项目初期,尽量使用较新但非最新的稳定版本组合,并记录下所有组件的具体版本号,便于团队统一和问题排查。
2.2 核心工具链选型解析
除了上述核心环境,一个高效的测试框架还需要一些辅助工具。这里的选择体现了框架的工程化程度。
1. 测试运行器:pytest vs unittest Python自带的 unittest 框架足够基础,但 pytest 更强大、更灵活,是目前社区的主流选择。
- pytest优势 :夹具(fixture)功能强大,可以优雅地管理测试前置(如启动App)和后置(如关闭App)条件;断言更智能,直接使用
assert语句;插件生态丰富(如并发执行、生成报告);测试发现规则更简单。 - 我们的选择 :使用
pytest。它能让我们以更少的代码,实现更清晰的测试结构和更强大的功能。例如,用一个@pytest.fixture装饰器就能管理整个Driver的生命周期。
2. 元素定位与调试工具:Appium Inspector 这是Appium Desktop自带的神器,相当于Web自动化中的“开发者工具”。它的作用是连接真实设备或模拟器,实时查看UI元素的属性(如resource-id, text, class, xpath等),并可以录制操作生成代码片段。在编写定位符时,它不可或缺。启动Appium Server后,在Appium Desktop中打开Inspector功能,填入正确的设备能力(Capabilities)即可连接。
3. 设备管理:ADB (Android Debug Bridge) ADB是你与Android设备(无论是真机还是模拟器)沟通的桥梁。常用命令如 adb devices 查看连接设备, adb install app.apk 安装应用, adb logcat 查看设备日志。熟练使用ADB命令,对于调试脚本、安装应用、抓取日志至关重要。
4. 集成开发环境(IDE):PyCharm 或 VS Code 两者皆可。PyCharm对Python支持更专业,开箱即用。VS Code更轻量,通过安装Python插件和Pytest插件也能获得极佳的体验。我个人偏好VS Code,因为它启动快,插件生态活跃,配合Git进行版本管理非常流畅。
3. 从零到一:构建你的第一个自动化脚本
环境就绪后,我们开始动手写第一个脚本。这个脚本的目标很简单:在一台Android设备上,打开系统自带的“计算器”应用,完成一次加法运算(如 8 + 2 = 10),并验证结果。
3.1 连接设备与初始化驱动
任何Appium脚本的开始,都是定义并初始化一个 WebDriver 对象,这个对象是我们控制设备的“遥控器”。
第一步:准备设备 确保你的Android设备(真机或模拟器)已通过USB连接电脑,并开启了“开发者选项”和“USB调试”模式。在命令行输入 adb devices ,应该能看到你的设备序列号,状态为 device 。
第二步:定义核心能力(Desired Capabilities) 这是告诉Appium Server“你要测试什么”的关键配置。它是一个字典(Dictionary)对象。
from appium import webdriver
desired_caps = {
'platformName': 'Android', # 测试平台
'platformVersion': '10', # 设备系统版本(根据你的设备修改)
'deviceName': 'your_device_name', # 设备名,adb devices查到的名称
'appPackage': 'com.android.calculator2', # 计算器应用的包名
'appActivity': '.Calculator', # 计算器应用的主Activity
'automationName': 'UiAutomator2', # Android自动化引擎,必选
'noReset': True, # 不重置应用状态(避免每次清空数据)
'newCommandTimeout': 600 # 命令超时时间(秒)
}
- 关键点解析 :
appPackage和appActivity:如何获取?有两种方法:1) 询问开发;2) 使用ADB命令。对于已安装的应用,在设备上打开它,然后在命令行输入adb shell dumpsys window | findstr mCurrentFocus(Windows)或adb shell dumpsys window | grep mCurrentFocus(Mac/Linux),输出结果中/后面的部分就是appPackage和appActivity。deviceName:在adb devices命令的输出中,第一列就是设备名。automationName: 对于Android 5.0 (API level 21) 以上,必须使用UiAutomator2,它是谷歌官方维护的引擎,比旧的UiAutomator1更稳定强大。
第三步:初始化驱动并启动会话
# 指定Appium Server的地址
driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps)
执行这行代码后,如果你的环境一切正常,你会看到设备上的计算器应用被自动启动。这标志着你已经成功建立了与设备的自动化会话。
3.2 元素定位与基础交互操作
应用启动后,我们需要操作界面上的元素(按钮)。在Appium中,我们通过“定位符”来找到这些元素。
1. 使用Appium Inspector查找元素属性 打开Appium Inspector,填入和脚本中一样的 desired_caps (注意 appPackage 和 appActivity ),点击“Start Session”。连接成功后,你就能看到计算器应用的界面结构树和截图。点击屏幕上的数字“8”按钮,右侧会显示这个元素的所有属性。
2. 编写定位与操作代码 假设我们通过Inspector发现,数字“8”按钮的 resource-id 是 com.android.calculator2:id/digit_8 ,加号按钮的 resource-id 是 com.android.calculator2:id/op_add ,等号按钮的 resource-id 是 com.android.calculator2:id/eq ,结果文本框的 resource-id 是 com.android.calculator2:id/result 。
那么,我们的操作脚本如下:
from appium.webdriver.common.appiumby import AppiumBy
import time
# 等待应用完全启动
time.sleep(2)
# 点击数字 8
eight_button = driver.find_element(AppiumBy.ID, 'com.android.calculator2:id/digit_8')
eight_button.click()
# 点击加号 +
plus_button = driver.find_element(AppiumBy.ID, 'com.android.calculator2:id/op_add')
plus_button.click()
# 点击数字 2
two_button = driver.find_element(AppiumBy.ID, 'com.android.calculator2:id/digit_2')
two_button.click()
# 点击等号 =
equals_button = driver.find_element(AppiumBy.ID, 'com.android.calculator2:id/eq')
equals_button.click()
# 获取结果文本框的内容
result_element = driver.find_element(AppiumBy.ID, 'com.android.calculator2:id/result')
actual_result = result_element.text
# 断言验证结果
expected_result = '10'
assert actual_result == expected_result, f'计算结果错误,期望{expected_result},实际得到{actual_result}'
print("测试通过!8 + 2 =", actual_result)
3. 交互操作详解
find_element: 用于查找单个元素。如果找不到元素,会抛出NoSuchElementException。对应的find_elements会返回一个列表。AppiumBy.ID: 这是通过元素的resource-id进行定位,是 首选且最稳定的定位方式 。其他常用定位方式还有:AppiumBy.XPATH: 通过路径表达式定位,功能强大但易受UI变动影响,慎用。AppiumBy.ACCESSIBILITY_ID: 对应元素的content-desc属性,对于无障碍支持好的应用,这也是一个不错的选择。AppiumBy.CLASS_NAME: 通过控件类名定位,如android.widget.Button,但通常不够精确。
click(): 最常用的点击操作。text属性:用于获取文本框(TextView)等元素显示的文本内容。
实操心得 :定位元素是自动化脚本稳定性的生命线。遵循以下优先级:1) ID定位优先 ;2) 其次考虑
accessibility_id;3) 万不得已再使用xpath,且尽量使用相对路径和非索引依赖的写法。每次UI迭代后,要优先检查核心元素的定位符是否依然有效。
3.3 添加等待与异常处理
上面的脚本使用了 time.sleep(2) ,这是一种“强制等待”,在简单场景下可行,但不推荐。因为它无论元素是否加载完成,都会死等固定时间,降低了脚本效率。
1. 智能等待(隐式等待与显式等待)
- 隐式等待 :在创建驱动后设置一次,对整个驱动生命周期有效。它会在查找元素时,如果立即没找到,会轮询等待一段时间,直到找到或超时。
driver.implicitly_wait(10) # 设置隐式等待10秒 - 显式等待 :针对某个特定条件进行等待,更加灵活精准。这是 推荐的最佳实践 。
显式等待可以等待元素可见、可点击、文本包含特定内容等复杂条件,使脚本更健壮。from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC # 等待结果元素出现并且其文本不为空 wait = WebDriverWait(driver, 10) result_element = wait.until( EC.presence_of_element_located((AppiumBy.ID, 'com.android.calculator2:id/result')) ) # 继续等待,直到结果文本不再是初始的‘0’(假设初始为0) wait.until(lambda driver: result_element.text != '0') actual_result = result_element.text
2. 基础异常处理 脚本可能因为网络波动、元素未找到、应用崩溃等原因失败。添加异常处理可以让脚本失败时给出更清晰的日志,而不是直接崩溃。
try:
element = driver.find_element(AppiumBy.ID, 'some_id')
element.click()
except NoSuchElementException:
print("错误:未在页面上找到ID为‘some_id’的元素,请检查UI是否已变更。")
# 这里可以附加截图操作,便于后续分析
driver.save_screenshot('error_screenshot.png')
except Exception as e:
print(f"发生未知错误:{e}")
将等待机制和异常处理融入你的脚本,是编写生产级自动化用例的基础。
4. 框架进阶:设计可维护的Page Object模式
当测试用例越来越多,你会发现脚本里充斥着大量的 find_element 和 click ,维护起来如同噩梦。一旦UI发生变化,你需要修改所有相关的用例。这时,就需要引入 Page Object(PO)设计模式 。它的核心思想是将“页面”抽象成一个类,页面上的 元素定位 和 基础操作 封装在这个类的方法里,而测试用例只关心“业务流”和“断言”。
4.1 Page Object模型设计与实现
我们以计算器应用为例,创建一个简单的PO模型目录结构:
project_root/
├── pages/
│ └── calculator_page.py # 计算器页面对象
├── tests/
│ └── test_calculator.py # 测试用例
├── conftest.py # pytest全局配置,如driver fixture
└── requirements.txt # 项目依赖
1. 创建页面对象类 ( pages/calculator_page.py )
from appium.webdriver.common.appiumby import AppiumBy
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
class CalculatorPage:
def __init__(self, driver):
self.driver = driver
# 定义所有元素的定位符,集中管理
self.digit_8 = (AppiumBy.ID, 'com.android.calculator2:id/digit_8')
self.digit_2 = (AppiumBy.ID, 'com.android.calculator2:id/digit_2')
self.op_add = (AppiumBy.ID, 'com.android.calculator2:id/op_add')
self.eq = (AppiumBy.ID, 'com.android.calculator2:id/eq')
self.result = (AppiumBy.ID, 'com.android.calculator2:id/result')
# 封装基础操作:点击数字8
def click_digit_8(self):
element = WebDriverWait(self.driver, 10).until(
EC.element_to_be_clickable(self.digit_8)
)
element.click()
return self # 支持链式调用
def click_digit_2(self):
self.driver.find_element(*self.digit_2).click()
return self
def click_plus(self):
self.driver.find_element(*self.op_add).click()
return self
def click_equals(self):
self.driver.find_element(*self.eq).click()
return self
# 封装获取结果的操作
def get_result(self):
# 使用显式等待确保结果已更新
result_element = WebDriverWait(self.driver, 10).until(
EC.presence_of_element_located(self.result)
)
# 可以在这里加入更复杂的等待逻辑,比如等待结果非初始值
WebDriverWait(self.driver, 5).until(
lambda d: result_element.text != '0' and result_element.text != ''
)
return result_element.text
在这个类里,所有关于计算器页面的细节(元素定位、等待逻辑)都被隐藏起来了。测试用例只需要调用像 click_digit_8() 这样语义清晰的方法。
2. 使用pytest fixture管理驱动生命周期 ( conftest.py ) conftest.py 是pytest的特有文件,其中定义的fixture可以被同一目录及子目录下的所有测试文件自动识别。
import pytest
from appium import webdriver
@pytest.fixture(scope='session') # scope='session'表示整个测试会话只启动一次驱动
def app_driver():
"""初始化并返回Appium驱动,测试结束后关闭"""
desired_caps = {
'platformName': 'Android',
'platformVersion': '10',
'deviceName': 'your_device_name',
'appPackage': 'com.android.calculator2',
'appActivity': '.Calculator',
'automationName': 'UiAutomator2',
'noReset': True,
'newCommandTimeout': 600
}
driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps)
driver.implicitly_wait(10)
yield driver # 测试用例在此处执行
print("测试结束,关闭驱动...")
driver.quit()
3. 编写清爽的测试用例 ( tests/test_calculator.py )
from pages.calculator_page import CalculatorPage
class TestCalculator:
def test_addition(self, app_driver): # 使用fixture ‘app_driver’
# 初始化页面对象
calc_page = CalculatorPage(app_driver)
# 业务流:8 + 2 =
calc_page.click_digit_8() \
.click_plus() \
.click_digit_2() \
.click_equals()
# 获取并断言结果
result = calc_page.get_result()
assert result == '10', f'加法运算失败,期望10,实际得到{result}'
# 可以轻松添加更多测试用例
def test_subtraction(self, app_driver):
# ... 调用页面对象封装好的减法操作(需先在CalculatorPage中添加)
pass
看,现在的测试用例多么清晰!它只关心测试逻辑:“点击8,点击加号,点击2,点击等号,然后验证结果是10”。所有底层的定位、等待、交互细节都被封装在 CalculatorPage 类中。当UI改变时,你只需要去修改 CalculatorPage 类中的定位符和相应方法,所有引用这个页面的测试用例都会自动生效,维护成本大大降低。
4.2 数据驱动与参数化测试
一个健壮的测试框架需要能方便地测试多组数据。pytest的 @pytest.mark.parametrize 装饰器完美支持这一点。
假设我们要测试多组加法运算,我们可以这样改造测试用例:
import pytest
class TestCalculatorDataDriven:
@pytest.mark.parametrize('num1, num2, expected', [
(8, 2, '10'),
(5, 3, '8'),
(9, 9, '18'),
(0, 0, '0'),
])
def test_addition_with_data(self, app_driver, num1, num2, expected):
calc_page = CalculatorPage(app_driver)
# 注意:这里需要扩展CalculatorPage,使其能点击任意数字
# 例如,添加一个 `click_digit(self, number)` 方法,内部通过数字映射到对应的元素ID
calc_page.click_digit(num1) \
.click_plus() \
.click_digit(num2) \
.click_equals()
result = calc_page.get_result()
assert result == expected, f'{num1} + {num2} 结果错误,期望{expected},实际得到{result}'
这样,我们只需要维护一组测试数据,就能自动生成并运行多个测试用例,极大地提高了测试的覆盖率和编写效率。
5. 工程化提升:配置管理、报告生成与持续集成雏形
一个只能在自己电脑上运行的脚本,还不能称之为“框架”。我们需要让它具备可配置、可报告、可集成的能力。
5.1 多环境配置管理
你的测试可能需要在不同的设备(Android/iOS)、不同版本的应用、甚至不同的测试服务器上运行。硬编码 desired_caps 是不可取的。我们可以使用配置文件(如YAML、JSON)来管理这些变量。
1. 创建配置文件 ( config/config.yaml )
environments:
android_emulator:
platformName: "Android"
platformVersion: "11"
deviceName: "Android_Emulator"
app: "${APK_PATH}/my_app_debug.apk" # 使用环境变量或绝对路径
appPackage: "com.example.myapp"
appActivity: ".MainActivity"
automationName: "UiAutomator2"
noReset: False
fullReset: False # 通常测试开始前清理数据
android_real_device:
platformName: "Android"
platformVersion: "10"
deviceName: "MI_8_UD" # 你的真机设备名
appPackage: "com.example.myapp"
appActivity: ".MainActivity"
automationName: "UiAutomator2"
noReset: True
2. 创建配置读取工具 ( utils/config_reader.py )
import yaml
import os
class ConfigReader:
def __init__(self, config_path='config/config.yaml'):
with open(config_path, 'r', encoding='utf-8') as f:
self.config = yaml.safe_load(f)
def get_capabilities(self, environment='android_emulator'):
"""获取指定环境的配置能力"""
caps = self.config['environments'].get(environment, {})
# 处理路径变量,例如将${APK_PATH}替换为实际环境变量值
if 'app' in caps and caps['app'].startswith('${'):
env_var = caps['app'][2:-1].split('}')[0] # 提取变量名,如APK_PATH
file_name = caps['app'].split('/')[-1]
caps['app'] = os.path.join(os.environ.get(env_var, ''), file_name)
return caps
def get_appium_server(self):
return self.config.get('appium_server', 'http://localhost:4723/wd/hub')
3. 在fixture中使用动态配置 ( conftest.py )
import pytest
from appium import webdriver
from utils.config_reader import ConfigReader
@pytest.fixture(scope='session')
def app_driver(request):
# 通过命令行参数或默认值决定使用哪个环境
env = request.config.getoption("--env", default="android_emulator")
config = ConfigReader()
desired_caps = config.get_capabilities(env)
server_url = config.get_appium_server()
driver = webdriver.Remote(server_url, desired_caps)
driver.implicitly_wait(10)
yield driver
driver.quit()
# 添加命令行选项
def pytest_addoption(parser):
parser.addoption(
"--env", action="store", default="android_emulator", help="设置测试环境: android_emulator 或 android_real_device"
)
现在,你可以通过命令行 pytest --env=android_real_device 来指定在真机上运行测试了。
5.2 生成美观的测试报告
测试结果不能只停留在控制台。我们需要一份直观、详细的HTML报告,包含通过率、失败原因、截图甚至日志。 pytest-html 和 allure-pytest 是两种主流选择。这里介绍更轻量级的 pytest-html 。
1. 安装插件
pip install pytest-html
2. 运行测试并生成报告
pytest tests/ --html=reports/report.html --self-contained-html
--self-contained-html 参数会将CSS和JS内嵌到HTML中,生成单个可独立打开的文件。
3. 在用例失败时自动截图 为了报告更有用,我们需要在测试失败时自动截屏。这可以通过修改 conftest.py 中的fixture或使用pytest的钩子函数实现。
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
"""
获取每个测试用例执行结果的钩子函数
"""
outcome = yield
rep = outcome.get_result()
# 只关注用例执行(call)阶段,且是失败或错误的情况
if rep.when == "call" and rep.failed:
# 获取driver fixture(需要确保测试用例使用了这个fixture)
driver_fixture = item.funcargs.get('app_driver')
if driver_fixture:
# 生成唯一的截图文件名
screenshot_name = f"screenshot_{item.name}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png"
screenshot_path = os.path.join('reports/screenshots', screenshot_name)
driver_fixture.save_screenshot(screenshot_path)
# 将截图路径添加到html报告中(需要pytest-html支持)
if hasattr(rep, 'extra'):
from pytest_html import extras
rep.extra.append(extras.image(screenshot_path))
rep.extra.append(extras.html(f'<div>失败截图:<a href="{screenshot_path}" target="_blank">{screenshot_name}</a></div>'))
这样,当用例失败时,报告里会直接显示截图链接,点击即可查看,极大方便了失败分析。
5.3 迈向持续集成(CI)
将你的自动化测试框架接入CI/CD流水线(如Jenkins, GitLab CI, GitHub Actions),是实现“无人值守”自动化测试的关键一步。核心思路是:当开发提交代码后,CI工具自动拉取代码,安装依赖,启动Appium Server(或在CI环境中使用已启动的Server),连接设备(可以是云测平台的真机或启动模拟器),执行测试脚本,并生成报告。
一个最简单的GitHub Actions工作流配置( .github/workflows/test.yml )示例如下:
name: Mobile UI Automation Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.9'
- name: Install dependencies
run: |
pip install -r requirements.txt
- name: Start Appium Server
run: |
npm install -g appium
appium --log-level error &
- name: Run tests
run: |
pytest tests/ --html=report.html
- name: Upload test report
uses: actions/upload-artifact@v2
with:
name: test-report
path: report.html
这只是一个起点,真实的CI环境还需要考虑Android模拟器的启动、多设备并行测试等更复杂的配置。
6. 实战避坑指南与性能优化
纸上得来终觉浅,绝知此事要躬行。下面分享一些我在实际项目中踩过的坑和总结的优化技巧,这些在官方文档里往往找不到。
6.1 常见问题与排查技巧实录
问题1: session not created / Unable to create a new remote session
- 可能原因 :
- Capabilities配置错误 :
appPackage/appActivity写错,platformVersion与设备不符。 - 应用未安装 :指定的应用未安装在设备上。
- 设备未连接/未授权 :
adb devices列表中设备状态不是device。 - 端口冲突 :4723端口被占用。
- Appium Server版本与客户端库不兼容 。
- Capabilities配置错误 :
- 排查步骤 :
- 核对
adb devices,确认设备在线且已授权USB调试。 - 使用
adb shell pm list packages | grep your.package.name检查应用是否安装。 - 使用Appium Inspector尝试用相同的Capabilities连接,看错误信息是否更详细。
- 检查Appium Server日志(启动时不要加
--log-level error),错误信息通常很明确。
- 核对
问题2:元素找不到( NoSuchElementException )
- 可能原因 :
- 定位符错误/失效 :UI改了,但定位符没更新。
- 页面未加载完成 :操作太快,元素还没出现。
- 元素在WebView或混合应用中 :需要切换上下文(Context)。
- 元素在弹窗或新Activity中 :需要等待新窗口出现。
- 排查与解决 :
- 优先使用ID定位 ,并和开发约定好给关键控件添加稳定的
resource-id。 - 用显式等待替代隐式等待和
sleep。 - 如果是混合应用,在操作前使用
driver.contexts获取所有上下文,并切换到对应的WebView上下文。 - 对于新窗口,可以等待特定的Activity出现:
driver.wait_activity(‘.NewActivity’, 10)。
- 优先使用ID定位 ,并和开发约定好给关键控件添加稳定的
问题3:脚本在真机上运行不稳定,时好时坏
- 可能原因 :
- 网络波动 :应用有网络请求,响应慢导致超时。
- 设备性能 :低端机卡顿,操作跟不上脚本速度。
- 动画干扰 :页面切换有动画,脚本在动画过程中操作。
- 优化策略 :
- 增加等待的宽容度 :适当延长显式等待的超时时间。
- 在点击等操作前,增加元素“可点击”的状态检查 :
EC.element_to_be_clickable。 - 关闭系统动画 :在开发者选项里,将“窗口动画缩放”、“过渡动画缩放”、“动画程序时长缩放”都设置为“关闭动画”。这能显著提升脚本稳定性。
- 使用更稳定的操作API :对于点击,可以尝试
driver.execute_script(‘mobile: click’, {‘element’: element.id})这个原生方法。
6.2 脚本稳定性与性能优化技巧
1. 使用唯一的定位策略 避免使用可能重复或变化的定位方式,如 xpath 中使用索引( //android.widget.Button[3] )或依赖文本( //*[@text=‘登录’] ,文本可能变化)。优先顺序: ID > accessibility_id > class name + 其他属性组合的 xpath 。
2. 封装通用的等待与重试机制 对于某些不稳定的操作(如网络请求后的页面刷新),可以封装一个带重试的点击函数。
def click_with_retry(driver, locator, max_retries=3):
for i in range(max_retries):
try:
element = WebDriverWait(driver, 5).until(EC.element_to_be_clickable(locator))
element.click()
return True
except (TimeoutException, StaleElementReferenceException):
print(f"第{i+1}次点击重试...")
time.sleep(1)
raise Exception(f"元素{locator}在{max_retries}次重试后仍无法点击")
3. 减少不必要的截图和日志 虽然截图对调试很重要,但频繁截图(尤其是全屏截图)会严重拖慢脚本速度。建议仅在用例开始、结束、失败或关键检查点时截图。
4. 并行测试 当测试套件很大时,串行执行会非常耗时。可以利用 pytest-xdist 插件进行并行测试,同时控制多台设备执行不同的测试用例。这需要CI环境和设备池(如Selenium Grid或Appium Grid)的支持,是大型项目提升效率的必经之路。
搭建移动端自动化测试框架不是一个一蹴而就的过程,而是一个不断迭代和优化的工程。从第一个“Hello World”脚本,到采用Page Object模式,再到配置化、报告化和CI集成,每一步都在提升框架的可用性和可维护性。最关键的还是动手去写,去踩坑,去解决真实项目中遇到的问题。你会发现,自动化不仅仅是替代手工操作,它更迫使你更深入地理解应用结构、更早地发现设计缺陷,最终和开发、产品一起,打造出质量更可靠的移动应用。
更多推荐
所有评论(0)